EasSyncService.java revision fa088c04714957a4ebcd02a588e09878bbf4dbd4
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.codec.binary.Base64; 21import com.android.email.mail.AuthenticationFailedException; 22import com.android.email.mail.MessagingException; 23import com.android.email.provider.EmailContent.Account; 24import com.android.email.provider.EmailContent.AccountColumns; 25import com.android.email.provider.EmailContent.Attachment; 26import com.android.email.provider.EmailContent.AttachmentColumns; 27import com.android.email.provider.EmailContent.HostAuth; 28import com.android.email.provider.EmailContent.Mailbox; 29import com.android.email.provider.EmailContent.MailboxColumns; 30import com.android.email.provider.EmailContent.Message; 31import com.android.exchange.adapter.AbstractSyncAdapter; 32import com.android.exchange.adapter.AccountSyncAdapter; 33import com.android.exchange.adapter.ContactsSyncAdapter; 34import com.android.exchange.adapter.EmailSyncAdapter; 35import com.android.exchange.adapter.FolderSyncParser; 36import com.android.exchange.adapter.PingParser; 37import com.android.exchange.adapter.Serializer; 38import com.android.exchange.adapter.Tags; 39import com.android.exchange.adapter.Parser.EasParserException; 40 41import org.apache.http.Header; 42import org.apache.http.HttpEntity; 43import org.apache.http.HttpResponse; 44import org.apache.http.HttpStatus; 45import org.apache.http.client.HttpClient; 46import org.apache.http.client.methods.HttpOptions; 47import org.apache.http.client.methods.HttpPost; 48import org.apache.http.client.methods.HttpRequestBase; 49import org.apache.http.conn.ClientConnectionManager; 50import org.apache.http.entity.ByteArrayEntity; 51import org.apache.http.impl.client.DefaultHttpClient; 52import org.apache.http.params.BasicHttpParams; 53import org.apache.http.params.HttpConnectionParams; 54import org.apache.http.params.HttpParams; 55 56import android.content.ContentResolver; 57import android.content.ContentUris; 58import android.content.ContentValues; 59import android.content.Context; 60import android.database.Cursor; 61import android.os.RemoteException; 62import android.os.SystemClock; 63 64import java.io.File; 65import java.io.FileOutputStream; 66import java.io.IOException; 67import java.io.InputStream; 68import java.net.URI; 69import java.net.URLEncoder; 70import java.security.cert.CertificateException; 71import java.util.ArrayList; 72import java.util.HashMap; 73 74public class EasSyncService extends AbstractSyncService { 75 private static final String EMAIL_WINDOW_SIZE = "5"; 76 public static final String PIM_WINDOW_SIZE = "5"; 77 private static final String WHERE_ACCOUNT_KEY_AND_SERVER_ID = 78 MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SERVER_ID + "=?"; 79 private static final String WHERE_ACCOUNT_AND_SYNC_INTERVAL_PING = 80 MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SYNC_INTERVAL + 81 '=' + Mailbox.CHECK_INTERVAL_PING; 82 private static final String AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX = " AND " + 83 MailboxColumns.SYNC_INTERVAL + " IN (" + Mailbox.CHECK_INTERVAL_PING + 84 ',' + Mailbox.CHECK_INTERVAL_PUSH + ") AND " + MailboxColumns.TYPE + "!=\"" + 85 Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + '\"'; 86 private static final String WHERE_PUSH_HOLD_NOT_ACCOUNT_MAILBOX = 87 MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SYNC_INTERVAL + 88 '=' + Mailbox.CHECK_INTERVAL_PUSH_HOLD; 89 static private final int CHUNK_SIZE = 16*1024; 90 91 static private final String PING_COMMAND = "Ping"; 92 static private final int COMMAND_TIMEOUT = 20*SECONDS; 93 94 // Define our default protocol version as 2.5 (Exchange 2003) 95 static private final String DEFAULT_PROTOCOL_VERSION = "2.5"; 96 97 /** 98 * We start with an 8 minute timeout, and increase/decrease by 3 minutes at a time. There's 99 * no point having a timeout shorter than 5 minutes, I think; at that point, we can just let 100 * the ping exception out. The maximum I use is 17 minutes, which is really an empirical 101 * choice; too long and we risk silent connection loss and loss of push for that period. Too 102 * short and we lose efficiency/battery life. 103 * 104 * If we ever have to drop the ping timeout, we'll never increase it again. There's no point 105 * going into hysteresis; the NAT timeout isn't going to change without a change in connection, 106 * which will cause the sync service to be restarted at the starting heartbeat and going through 107 * the process again. 108 */ 109 static private final int PING_MINUTES = 60; // in seconds 110 static private final int PING_FUDGE_LOW = 10; 111 static private final int PING_STARTING_HEARTBEAT = (8*PING_MINUTES)-PING_FUDGE_LOW; 112 static private final int PING_MIN_HEARTBEAT = (5*PING_MINUTES)-PING_FUDGE_LOW; 113 static private final int PING_MAX_HEARTBEAT = (17*PING_MINUTES)-PING_FUDGE_LOW; 114 static private final int PING_HEARTBEAT_INCREMENT = 3*PING_MINUTES; 115 static private final int PING_FORCE_HEARTBEAT = 2*PING_MINUTES; 116 117 static private final int PROTOCOL_PING_STATUS_COMPLETED = 1; 118 119 // Fallbacks (in minutes) for ping loop failures 120 static private final int MAX_PING_FAILURES = 1; 121 static private final int PING_FALLBACK_INBOX = 5; 122 static private final int PING_FALLBACK_PIM = 25; 123 124 // Reasonable default 125 public String mProtocolVersion = DEFAULT_PROTOCOL_VERSION; 126 public Double mProtocolVersionDouble; 127 protected String mDeviceId = null; 128 private String mDeviceType = "Android"; 129 private String mAuthString = null; 130 private String mCmdString = null; 131 public String mHostAddress; 132 public String mUserName; 133 public String mPassword; 134 private boolean mSsl = true; 135 private boolean mTrustSsl = false; 136 public ContentResolver mContentResolver; 137 private String[] mBindArguments = new String[2]; 138 private ArrayList<String> mPingChangeList; 139 private HttpPost mPendingPost = null; 140 // The ping time (in seconds) 141 private int mPingHeartbeat = PING_STARTING_HEARTBEAT; 142 // The longest successful ping heartbeat 143 private int mPingHighWaterMark = 0; 144 // Whether we've ever lowered the heartbeat 145 private boolean mPingHeartbeatDropped = false; 146 // Whether a POST was aborted due to watchdog timeout 147 private boolean mPostAborted = false; 148 // Whether or not the sync service is valid (usable) 149 public boolean mIsValid = true; 150 151 public EasSyncService(Context _context, Mailbox _mailbox) { 152 super(_context, _mailbox); 153 mContentResolver = _context.getContentResolver(); 154 if (mAccount == null) { 155 mIsValid = false; 156 return; 157 } 158 HostAuth ha = HostAuth.restoreHostAuthWithId(_context, mAccount.mHostAuthKeyRecv); 159 if (ha == null) { 160 mIsValid = false; 161 return; 162 } 163 mSsl = (ha.mFlags & HostAuth.FLAG_SSL) != 0; 164 mTrustSsl = (ha.mFlags & HostAuth.FLAG_TRUST_ALL_CERTIFICATES) != 0; 165 } 166 167 private EasSyncService(String prefix) { 168 super(prefix); 169 } 170 171 public EasSyncService() { 172 this("EAS Validation"); 173 } 174 175 @Override 176 public void ping() { 177 userLog("Alarm ping received!"); 178 synchronized(getSynchronizer()) { 179 if (mPendingPost != null) { 180 userLog("Aborting pending POST!"); 181 mPostAborted = true; 182 mPendingPost.abort(); 183 } 184 } 185 } 186 187 @Override 188 public void stop() { 189 mStop = true; 190 synchronized(getSynchronizer()) { 191 if (mPendingPost != null) { 192 mPendingPost.abort(); 193 } 194 } 195 } 196 197 /** 198 * Determine whether an HTTP code represents an authentication error 199 * @param code the HTTP code returned by the server 200 * @return whether or not the code represents an authentication error 201 */ 202 protected boolean isAuthError(int code) { 203 return ((code == HttpStatus.SC_UNAUTHORIZED) || (code == HttpStatus.SC_FORBIDDEN)); 204 } 205 206 @Override 207 public void validateAccount(String hostAddress, String userName, String password, int port, 208 boolean ssl, boolean trustCertificates, Context context) throws MessagingException { 209 try { 210 userLog("Testing EAS: ", hostAddress, ", ", userName, ", ssl = ", ssl ? "1" : "0"); 211 EasSyncService svc = new EasSyncService("%TestAccount%"); 212 svc.mContext = context; 213 svc.mHostAddress = hostAddress; 214 svc.mUserName = userName; 215 svc.mPassword = password; 216 svc.mSsl = ssl; 217 svc.mTrustSsl = trustCertificates; 218 svc.mDeviceId = SyncManager.getDeviceId(); 219 HttpResponse resp = svc.sendHttpClientOptions(); 220 int code = resp.getStatusLine().getStatusCode(); 221 userLog("Validation (OPTIONS) response: " + code); 222 if (code == HttpStatus.SC_OK) { 223 // No exception means successful validation 224 Header commands = resp.getFirstHeader("MS-ASProtocolCommands"); 225 Header versions = resp.getFirstHeader("ms-asprotocolversions"); 226 if (commands == null || versions == null) { 227 userLog("OPTIONS response without commands or versions; reporting I/O error"); 228 throw new MessagingException(MessagingException.IOERROR); 229 } 230 231 // Run second test here for provisioning failures... 232 Serializer s = new Serializer(); 233 userLog("Try folder sync"); 234 s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY).text("0") 235 .end().end().done(); 236 resp = svc.sendHttpClientPost("FolderSync", s.toByteArray()); 237 code = resp.getStatusLine().getStatusCode(); 238 if (code == HttpStatus.SC_FORBIDDEN) { 239 throw new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED); 240 } 241 userLog("Validation successful"); 242 return; 243 } 244 if (isAuthError(code)) { 245 userLog("Authentication failed"); 246 throw new AuthenticationFailedException("Validation failed"); 247 } else { 248 // TODO Need to catch other kinds of errors (e.g. policy) For now, report the code. 249 userLog("Validation failed, reporting I/O error: ", code); 250 throw new MessagingException(MessagingException.IOERROR); 251 } 252 } catch (IOException e) { 253 Throwable cause = e.getCause(); 254 if (cause != null && cause instanceof CertificateException) { 255 userLog("CertificateException caught: ", e.getMessage()); 256 throw new MessagingException(MessagingException.GENERAL_SECURITY); 257 } 258 userLog("IOException caught: ", e.getMessage()); 259 throw new MessagingException(MessagingException.IOERROR); 260 } 261 262 } 263 264 private void doStatusCallback(long messageId, long attachmentId, int status) { 265 try { 266 SyncManager.callback().loadAttachmentStatus(messageId, attachmentId, status, 0); 267 } catch (RemoteException e) { 268 // No danger if the client is no longer around 269 } 270 } 271 272 private void doProgressCallback(long messageId, long attachmentId, int progress) { 273 try { 274 SyncManager.callback().loadAttachmentStatus(messageId, attachmentId, 275 EmailServiceStatus.IN_PROGRESS, progress); 276 } catch (RemoteException e) { 277 // No danger if the client is no longer around 278 } 279 } 280 281 public File createUniqueFileInternal(String dir, String filename) { 282 File directory; 283 if (dir == null) { 284 directory = mContext.getFilesDir(); 285 } else { 286 directory = new File(dir); 287 } 288 if (!directory.exists()) { 289 directory.mkdirs(); 290 } 291 File file = new File(directory, filename); 292 if (!file.exists()) { 293 return file; 294 } 295 // Get the extension of the file, if any. 296 int index = filename.lastIndexOf('.'); 297 String name = filename; 298 String extension = ""; 299 if (index != -1) { 300 name = filename.substring(0, index); 301 extension = filename.substring(index); 302 } 303 for (int i = 2; i < Integer.MAX_VALUE; i++) { 304 file = new File(directory, name + '-' + i + extension); 305 if (!file.exists()) { 306 return file; 307 } 308 } 309 return null; 310 } 311 312 /** 313 * Loads an attachment, based on the PartRequest passed in. The PartRequest is basically our 314 * wrapper for Attachment 315 * @param req the part (attachment) to be retrieved 316 * @throws IOException 317 */ 318 protected void getAttachment(PartRequest req) throws IOException { 319 Attachment att = req.att; 320 Message msg = Message.restoreMessageWithId(mContext, att.mMessageKey); 321 doProgressCallback(msg.mId, att.mId, 0); 322 323 String cmd = "GetAttachment&AttachmentName=" + att.mLocation; 324 HttpResponse res = sendHttpClientPost(cmd, null, COMMAND_TIMEOUT); 325 326 int status = res.getStatusLine().getStatusCode(); 327 if (status == HttpStatus.SC_OK) { 328 HttpEntity e = res.getEntity(); 329 int len = (int)e.getContentLength(); 330 InputStream is = res.getEntity().getContent(); 331 File f = (req.destination != null) 332 ? new File(req.destination) 333 : createUniqueFileInternal(req.destination, att.mFileName); 334 if (f != null) { 335 // Ensure that the target directory exists 336 File destDir = f.getParentFile(); 337 if (!destDir.exists()) { 338 destDir.mkdirs(); 339 } 340 FileOutputStream os = new FileOutputStream(f); 341 // len > 0 means that Content-Length was set in the headers 342 // len < 0 means "chunked" transfer-encoding 343 if (len != 0) { 344 try { 345 mPendingPartRequest = req; 346 byte[] bytes = new byte[CHUNK_SIZE]; 347 int length = len; 348 // Loop terminates 1) when EOF is reached or 2) if an IOException occurs 349 // One of these is guaranteed to occur 350 int totalRead = 0; 351 userLog("Attachment content-length: ", len); 352 while (true) { 353 int read = is.read(bytes, 0, CHUNK_SIZE); 354 355 // read < 0 means that EOF was reached 356 if (read < 0) { 357 userLog("Attachment load reached EOF, totalRead: ", totalRead); 358 break; 359 } 360 361 // Keep track of how much we've read for progress callback 362 totalRead += read; 363 364 // Write these bytes out 365 os.write(bytes, 0, read); 366 367 // We can't report percentages if this is chunked; by definition, the 368 // length of incoming data is unknown 369 if (length > 0) { 370 // Belt and suspenders check to prevent runaway reading 371 if (totalRead > length) { 372 errorLog("totalRead is greater than attachment length?"); 373 break; 374 } 375 int pct = (totalRead * 100 / length); 376 doProgressCallback(msg.mId, att.mId, pct); 377 } 378 } 379 } finally { 380 mPendingPartRequest = null; 381 } 382 } 383 os.flush(); 384 os.close(); 385 386 // EmailProvider will throw an exception if we try to update an unsaved attachment 387 if (att.isSaved()) { 388 String contentUriString = (req.contentUriString != null) 389 ? req.contentUriString 390 : "file://" + f.getAbsolutePath(); 391 ContentValues cv = new ContentValues(); 392 cv.put(AttachmentColumns.CONTENT_URI, contentUriString); 393 att.update(mContext, cv); 394 doStatusCallback(msg.mId, att.mId, EmailServiceStatus.SUCCESS); 395 } 396 } 397 } else { 398 doStatusCallback(msg.mId, att.mId, EmailServiceStatus.MESSAGE_NOT_FOUND); 399 } 400 } 401 402 @SuppressWarnings("deprecation") 403 private String makeUriString(String cmd, String extra) throws IOException { 404 // Cache the authentication string and the command string 405 String safeUserName = URLEncoder.encode(mUserName); 406 if (mAuthString == null) { 407 String cs = mUserName + ':' + mPassword; 408 mAuthString = "Basic " + new String(Base64.encodeBase64(cs.getBytes())); 409 mCmdString = "&User=" + safeUserName + "&DeviceId=" + mDeviceId + "&DeviceType=" 410 + mDeviceType; 411 } 412 String us = (mSsl ? (mTrustSsl ? "httpts" : "https") : "http") + "://" + mHostAddress + 413 "/Microsoft-Server-ActiveSync"; 414 if (cmd != null) { 415 us += "?Cmd=" + cmd + mCmdString; 416 } 417 if (extra != null) { 418 us += extra; 419 } 420 return us; 421 } 422 423 private void setHeaders(HttpRequestBase method) { 424 method.setHeader("Authorization", mAuthString); 425 method.setHeader("MS-ASProtocolVersion", mProtocolVersion); 426 method.setHeader("Connection", "keep-alive"); 427 method.setHeader("User-Agent", mDeviceType + '/' + Eas.VERSION); 428 } 429 430 private ClientConnectionManager getClientConnectionManager() { 431 return SyncManager.getClientConnectionManager(); 432 } 433 434 private HttpClient getHttpClient(int timeout) { 435 HttpParams params = new BasicHttpParams(); 436 HttpConnectionParams.setConnectionTimeout(params, 15*SECONDS); 437 HttpConnectionParams.setSoTimeout(params, timeout); 438 HttpConnectionParams.setSocketBufferSize(params, 8192); 439 HttpClient client = new DefaultHttpClient(getClientConnectionManager(), params); 440 return client; 441 } 442 443 protected HttpResponse sendHttpClientPost(String cmd, byte[] bytes) throws IOException { 444 return sendHttpClientPost(cmd, new ByteArrayEntity(bytes), COMMAND_TIMEOUT); 445 } 446 447 protected HttpResponse sendHttpClientPost(String cmd, HttpEntity entity) throws IOException { 448 return sendHttpClientPost(cmd, entity, COMMAND_TIMEOUT); 449 } 450 451 protected HttpResponse sendPing(byte[] bytes, int heartbeat) throws IOException { 452 Thread.currentThread().setName(mAccount.mDisplayName + ": Ping"); 453 if (Eas.USER_LOG) { 454 userLog("Send ping, timeout: " + heartbeat + "s, high: " + mPingHighWaterMark + 's'); 455 } 456 return sendHttpClientPost(PING_COMMAND, new ByteArrayEntity(bytes), (heartbeat+5)*SECONDS); 457 } 458 459 protected HttpResponse sendHttpClientPost(String cmd, HttpEntity entity, int timeout) 460 throws IOException { 461 HttpClient client = getHttpClient(timeout); 462 boolean sleepAllowed = cmd.equals(PING_COMMAND); 463 464 // Split the mail sending commands 465 String extra = null; 466 boolean msg = false; 467 if (cmd.startsWith("SmartForward&") || cmd.startsWith("SmartReply&")) { 468 int cmdLength = cmd.indexOf('&'); 469 extra = cmd.substring(cmdLength); 470 cmd = cmd.substring(0, cmdLength); 471 msg = true; 472 } else if (cmd.startsWith("SendMail&")) { 473 msg = true; 474 } 475 476 String us = makeUriString(cmd, extra); 477 HttpPost method = new HttpPost(URI.create(us)); 478 // Send the proper Content-Type header 479 // If entity is null (e.g. for attachments), don't set this header 480 if (msg) { 481 method.setHeader("Content-Type", "message/rfc822"); 482 } else if (entity != null) { 483 method.setHeader("Content-Type", "application/vnd.ms-sync.wbxml"); 484 } 485 setHeaders(method); 486 method.setEntity(entity); 487 synchronized(getSynchronizer()) { 488 mPendingPost = method; 489 if (sleepAllowed) { 490 SyncManager.runAsleep(mMailboxId, timeout+(10*SECONDS)); 491 } 492 } 493 try { 494 return client.execute(method); 495 } finally { 496 synchronized(getSynchronizer()) { 497 if (sleepAllowed) { 498 SyncManager.runAwake(mMailboxId); 499 } 500 mPendingPost = null; 501 } 502 } 503 } 504 505 protected HttpResponse sendHttpClientOptions() throws IOException { 506 HttpClient client = getHttpClient(COMMAND_TIMEOUT); 507 String us = makeUriString("OPTIONS", null); 508 HttpOptions method = new HttpOptions(URI.create(us)); 509 setHeaders(method); 510 return client.execute(method); 511 } 512 513 String getTargetCollectionClassFromCursor(Cursor c) { 514 int type = c.getInt(Mailbox.CONTENT_TYPE_COLUMN); 515 if (type == Mailbox.TYPE_CONTACTS) { 516 return "Contacts"; 517 } else if (type == Mailbox.TYPE_CALENDAR) { 518 return "Calendar"; 519 } else { 520 return "Email"; 521 } 522 } 523 524 /** 525 * Performs FolderSync 526 * 527 * @throws IOException 528 * @throws EasParserException 529 */ 530 public void runAccountMailbox() throws IOException, EasParserException { 531 // Initialize exit status to success 532 mExitStatus = EmailServiceStatus.SUCCESS; 533 try { 534 try { 535 SyncManager.callback() 536 .syncMailboxListStatus(mAccount.mId, EmailServiceStatus.IN_PROGRESS, 0); 537 } catch (RemoteException e1) { 538 // Don't care if this fails 539 } 540 541 if (mAccount.mSyncKey == null) { 542 mAccount.mSyncKey = "0"; 543 userLog("Account syncKey INIT to 0"); 544 ContentValues cv = new ContentValues(); 545 cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey); 546 mAccount.update(mContext, cv); 547 } 548 549 boolean firstSync = mAccount.mSyncKey.equals("0"); 550 if (firstSync) { 551 userLog("Initial FolderSync"); 552 } 553 554 // When we first start up, change all mailboxes to push. 555 ContentValues cv = new ContentValues(); 556 cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH); 557 if (mContentResolver.update(Mailbox.CONTENT_URI, cv, 558 WHERE_ACCOUNT_AND_SYNC_INTERVAL_PING, 559 new String[] {Long.toString(mAccount.mId)}) > 0) { 560 SyncManager.kick("change ping boxes to push"); 561 } 562 563 // Determine our protocol version, if we haven't already and save it in the Account 564 if (mAccount.mProtocolVersion == null) { 565 userLog("Determine EAS protocol version"); 566 HttpResponse resp = sendHttpClientOptions(); 567 int code = resp.getStatusLine().getStatusCode(); 568 userLog("OPTIONS response: ", code); 569 if (code == HttpStatus.SC_OK) { 570 Header header = resp.getFirstHeader("MS-ASProtocolCommands"); 571 userLog(header.getValue()); 572 header = resp.getFirstHeader("ms-asprotocolversions"); 573 String versions = header.getValue(); 574 if (versions != null) { 575 if (versions.contains("12.0")) { 576 mProtocolVersion = "12.0"; 577 } 578 mProtocolVersionDouble = Double.parseDouble(mProtocolVersion); 579 mAccount.mProtocolVersion = mProtocolVersion; 580 // Save the protocol version 581 cv.clear(); 582 cv.put(Account.PROTOCOL_VERSION, mProtocolVersion); 583 mAccount.update(mContext, cv); 584 userLog(versions); 585 userLog("Using version ", mProtocolVersion); 586 } else { 587 errorLog("No protocol versions in OPTIONS response"); 588 throw new IOException(); 589 } 590 } else { 591 errorLog("OPTIONS command failed; throwing IOException"); 592 throw new IOException(); 593 } 594 } 595 596 // Change all pushable boxes to push when we start the account mailbox 597 if (mAccount.mSyncInterval == Account.CHECK_INTERVAL_PUSH) { 598 cv.clear(); 599 cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH); 600 if (mContentResolver.update(Mailbox.CONTENT_URI, cv, 601 SyncManager.WHERE_IN_ACCOUNT_AND_PUSHABLE, 602 new String[] {Long.toString(mAccount.mId)}) > 0) { 603 userLog("Push account; set pushable boxes to push..."); 604 } 605 } 606 607 while (!mStop) { 608 userLog("Sending Account syncKey: ", mAccount.mSyncKey); 609 Serializer s = new Serializer(); 610 s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY) 611 .text(mAccount.mSyncKey).end().end().done(); 612 HttpResponse resp = sendHttpClientPost("FolderSync", s.toByteArray()); 613 if (mStop) break; 614 int code = resp.getStatusLine().getStatusCode(); 615 if (code == HttpStatus.SC_OK) { 616 HttpEntity entity = resp.getEntity(); 617 int len = (int)entity.getContentLength(); 618 if (len != 0) { 619 InputStream is = entity.getContent(); 620 // Returns true if we need to sync again 621 if (new FolderSyncParser(is, new AccountSyncAdapter(mMailbox, this)) 622 .parse()) { 623 continue; 624 } 625 } 626 } else if (isAuthError(code)) { 627 mExitStatus = EXIT_LOGIN_FAILURE; 628 } else { 629 userLog("FolderSync response error: ", code); 630 } 631 632 // Change all push/hold boxes to push 633 cv.clear(); 634 cv.put(Mailbox.SYNC_INTERVAL, Account.CHECK_INTERVAL_PUSH); 635 if (mContentResolver.update(Mailbox.CONTENT_URI, cv, 636 WHERE_PUSH_HOLD_NOT_ACCOUNT_MAILBOX, 637 new String[] {Long.toString(mAccount.mId)}) > 0) { 638 userLog("Set push/hold boxes to push..."); 639 } 640 641 try { 642 SyncManager.callback() 643 .syncMailboxListStatus(mAccount.mId, mExitStatus, 0); 644 } catch (RemoteException e1) { 645 // Don't care if this fails 646 } 647 648 // Wait for push notifications. 649 String threadName = Thread.currentThread().getName(); 650 try { 651 runPingLoop(); 652 } catch (StaleFolderListException e) { 653 // We break out if we get told about a stale folder list 654 userLog("Ping interrupted; folder list requires sync..."); 655 } finally { 656 Thread.currentThread().setName(threadName); 657 } 658 } 659 } catch (IOException e) { 660 // We catch this here to send the folder sync status callback 661 // A folder sync failed callback will get sent from run() 662 try { 663 if (!mStop) { 664 SyncManager.callback() 665 .syncMailboxListStatus(mAccount.mId, 666 EmailServiceStatus.CONNECTION_ERROR, 0); 667 } 668 } catch (RemoteException e1) { 669 // Don't care if this fails 670 } 671 throw e; 672 } 673 } 674 675 void pushFallback(long mailboxId) { 676 Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId); 677 ContentValues cv = new ContentValues(); 678 int mins = PING_FALLBACK_PIM; 679 if (mailbox.mType == Mailbox.TYPE_INBOX) { 680 mins = PING_FALLBACK_INBOX; 681 } 682 cv.put(Mailbox.SYNC_INTERVAL, mins); 683 mContentResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId), 684 cv, null, null); 685 errorLog("*** PING ERROR LOOP: Set " + mailbox.mDisplayName + " to " + mins + " min sync"); 686 SyncManager.kick("push fallback"); 687 } 688 689 void runPingLoop() throws IOException, StaleFolderListException { 690 int pingHeartbeat = mPingHeartbeat; 691 userLog("runPingLoop"); 692 // Do push for all sync services here 693 long endTime = System.currentTimeMillis() + (30*MINUTES); 694 HashMap<String, Integer> pingErrorMap = new HashMap<String, Integer>(); 695 ArrayList<String> readyMailboxes = new ArrayList<String>(); 696 ArrayList<String> notReadyMailboxes = new ArrayList<String>(); 697 int pingWaitCount = 0; 698 699 while ((System.currentTimeMillis() < endTime) && !mStop) { 700 // Count of pushable mailboxes 701 int pushCount = 0; 702 // Count of mailboxes that can be pushed right now 703 int canPushCount = 0; 704 // Count of uninitialized boxes 705 int uninitCount = 0; 706 707 Serializer s = new Serializer(); 708 Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, 709 MailboxColumns.ACCOUNT_KEY + '=' + mAccount.mId + 710 AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX, null, null); 711 notReadyMailboxes.clear(); 712 readyMailboxes.clear(); 713 try { 714 // Loop through our pushed boxes seeing what is available to push 715 while (c.moveToNext()) { 716 pushCount++; 717 // Two requirements for push: 718 // 1) SyncManager tells us the mailbox is syncable (not running, not stopped) 719 // 2) The syncKey isn't "0" (i.e. it's synced at least once) 720 long mailboxId = c.getLong(Mailbox.CONTENT_ID_COLUMN); 721 int pingStatus = SyncManager.pingStatus(mailboxId); 722 String mailboxName = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN); 723 if (pingStatus == SyncManager.PING_STATUS_OK) { 724 725 String syncKey = c.getString(Mailbox.CONTENT_SYNC_KEY_COLUMN); 726 if ((syncKey == null) || syncKey.equals("0")) { 727 // We can't push until the initial sync is done 728 pushCount--; 729 uninitCount++; 730 continue; 731 } 732 733 if (canPushCount++ == 0) { 734 // Initialize the Ping command 735 s.start(Tags.PING_PING) 736 .data(Tags.PING_HEARTBEAT_INTERVAL, 737 Integer.toString(pingHeartbeat)) 738 .start(Tags.PING_FOLDERS); 739 } 740 741 String folderClass = getTargetCollectionClassFromCursor(c); 742 s.start(Tags.PING_FOLDER) 743 .data(Tags.PING_ID, c.getString(Mailbox.CONTENT_SERVER_ID_COLUMN)) 744 .data(Tags.PING_CLASS, folderClass) 745 .end(); 746 readyMailboxes.add(mailboxName); 747 } else if ((pingStatus == SyncManager.PING_STATUS_RUNNING) || 748 (pingStatus == SyncManager.PING_STATUS_WAITING)) { 749 notReadyMailboxes.add(mailboxName); 750 } else if (pingStatus == SyncManager.PING_STATUS_UNABLE) { 751 pushCount--; 752 userLog(mailboxName, " in error state; ignore"); 753 continue; 754 } 755 } 756 } finally { 757 c.close(); 758 } 759 760 if (Eas.USER_LOG) { 761 if (!notReadyMailboxes.isEmpty()) { 762 userLog("Ping not ready for: " + notReadyMailboxes); 763 } 764 if (!readyMailboxes.isEmpty()) { 765 userLog("Ping ready for: " + readyMailboxes); 766 } 767 } 768 769 // If we've waited 10 seconds or more, just ping with whatever boxes are ready 770 // But use a shorter than normal heartbeat 771 boolean forcePing = !notReadyMailboxes.isEmpty() && (pingWaitCount > 5); 772 773 if ((canPushCount > 0) && ((canPushCount == pushCount) || forcePing)) { 774 // If all pingable boxes are ready for push, send Ping to the server 775 s.end().end().done(); 776 pingWaitCount = 0; 777 778 // If we've been stopped, this is a good time to return 779 if (mStop) return; 780 781 long pingTime = SystemClock.elapsedRealtime(); 782 try { 783 // Send the ping, wrapped by appropriate timeout/alarm 784 if (forcePing) { 785 userLog("Forcing ping after waiting for all boxes to be ready"); 786 } 787 HttpResponse res = 788 sendPing(s.toByteArray(), forcePing ? PING_FORCE_HEARTBEAT : pingHeartbeat); 789 790 int code = res.getStatusLine().getStatusCode(); 791 userLog("Ping response: ", code); 792 793 // Return immediately if we've been asked to stop during the ping 794 if (mStop) { 795 userLog("Stopping pingLoop"); 796 return; 797 } 798 799 if (code == HttpStatus.SC_OK) { 800 HttpEntity e = res.getEntity(); 801 int len = (int)e.getContentLength(); 802 InputStream is = res.getEntity().getContent(); 803 if (len != 0) { 804 int pingResult = parsePingResult(is, mContentResolver, pingErrorMap); 805 // If our ping completed (status = 1), and we weren't forced and we're 806 // not at the maximum, try increasing timeout by two minutes 807 if (pingResult == PROTOCOL_PING_STATUS_COMPLETED && !forcePing) { 808 if (pingHeartbeat > mPingHighWaterMark) { 809 mPingHighWaterMark = pingHeartbeat; 810 userLog("Setting high water mark at: ", mPingHighWaterMark); 811 } 812 if ((pingHeartbeat < PING_MAX_HEARTBEAT) && 813 !mPingHeartbeatDropped) { 814 pingHeartbeat += PING_HEARTBEAT_INCREMENT; 815 if (pingHeartbeat > PING_MAX_HEARTBEAT) { 816 pingHeartbeat = PING_MAX_HEARTBEAT; 817 } 818 userLog("Increasing ping heartbeat to ", pingHeartbeat, "s"); 819 } 820 } 821 } else { 822 userLog("Ping returned empty result; throwing IOException"); 823 throw new IOException(); 824 } 825 } else if (isAuthError(code)) { 826 mExitStatus = EXIT_LOGIN_FAILURE; 827 userLog("Authorization error during Ping: ", code); 828 throw new IOException(); 829 } 830 } catch (IOException e) { 831 String message = e.getMessage(); 832 // If we get the exception that is indicative of a NAT timeout and if we 833 // haven't yet "fixed" the timeout, back off by two minutes and "fix" it 834 boolean hasMessage = message != null; 835 userLog("IOException runPingLoop: " + (hasMessage ? message : "[no message]")); 836 if (mPostAborted || (hasMessage && message.contains("reset by peer"))) { 837 long pingLength = SystemClock.elapsedRealtime() - pingTime; 838 if ((pingHeartbeat > PING_MIN_HEARTBEAT) && 839 (pingHeartbeat > mPingHighWaterMark)) { 840 pingHeartbeat -= PING_HEARTBEAT_INCREMENT; 841 mPingHeartbeatDropped = true; 842 if (pingHeartbeat < PING_MIN_HEARTBEAT) { 843 pingHeartbeat = PING_MIN_HEARTBEAT; 844 } 845 userLog("Decreased ping heartbeat to ", pingHeartbeat, "s"); 846 } else if (mPostAborted || (pingLength < 2000)) { 847 userLog("Abort or NAT type return < 2 seconds; throwing IOException"); 848 throw e; 849 } else { 850 userLog("NAT type IOException > 2 seconds?"); 851 } 852 } else { 853 throw e; 854 } 855 } 856 } else if (forcePing) { 857 // In this case, there aren't any boxes that are pingable, but there are boxes 858 // waiting (for IOExceptions) 859 userLog("pingLoop waiting 60s for any pingable boxes"); 860 sleep(60*SECONDS, true); 861 } else if (pushCount > 0) { 862 // If we want to Ping, but can't just yet, wait a little bit 863 // TODO Change sleep to wait and use notify from SyncManager when a sync ends 864 sleep(2*SECONDS, false); 865 pingWaitCount++; 866 //userLog("pingLoop waited 2s for: ", (pushCount - canPushCount), " box(es)"); 867 } else if (uninitCount > 0) { 868 // In this case, we're doing an initial sync of at least one mailbox. Since this 869 // is typically a one-time case, I'm ok with trying again every 10 seconds until 870 // we're in one of the other possible states. 871 userLog("pingLoop waiting for initial sync of ", uninitCount, " box(es)"); 872 sleep(10*SECONDS, true); 873 } else { 874 // We've got nothing to do, so we'll check again in 30 minutes at which time 875 // we'll update the folder list. Let the device sleep in the meantime... 876 userLog("pingLoop sleeping for 30m"); 877 sleep(30*MINUTES, true); 878 } 879 } 880 } 881 882 void sleep(long ms, boolean runAsleep) { 883 if (runAsleep) { 884 SyncManager.runAsleep(mMailboxId, ms+(5*SECONDS)); 885 } 886 try { 887 Thread.sleep(ms); 888 } catch (InterruptedException e) { 889 // Doesn't matter whether we stop early; it's the thought that counts 890 } finally { 891 if (runAsleep) { 892 SyncManager.runAwake(mMailboxId); 893 } 894 } 895 } 896 897 private int parsePingResult(InputStream is, ContentResolver cr, 898 HashMap<String, Integer> errorMap) 899 throws IOException, StaleFolderListException { 900 PingParser pp = new PingParser(is, this); 901 if (pp.parse()) { 902 // True indicates some mailboxes need syncing... 903 // syncList has the serverId's of the mailboxes... 904 mBindArguments[0] = Long.toString(mAccount.mId); 905 mPingChangeList = pp.getSyncList(); 906 for (String serverId: mPingChangeList) { 907 mBindArguments[1] = serverId; 908 Cursor c = cr.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, 909 WHERE_ACCOUNT_KEY_AND_SERVER_ID, mBindArguments, null); 910 try { 911 if (c.moveToFirst()) { 912 913 /** 914 * Check the boxes reporting changes to see if there really were any... 915 * We do this because bugs in various Exchange servers can put us into a 916 * looping behavior by continually reporting changes in a mailbox, even when 917 * there aren't any. 918 * 919 * This behavior is seemingly random, and therefore we must code defensively 920 * by backing off of push behavior when it is detected. 921 * 922 * One known cause, on certain Exchange 2003 servers, is acknowledged by 923 * Microsoft, and the server hotfix for this case can be found at 924 * http://support.microsoft.com/kb/923282 925 */ 926 927 // Check the status of the last sync 928 String status = c.getString(Mailbox.CONTENT_SYNC_STATUS_COLUMN); 929 int type = SyncManager.getStatusType(status); 930 // This check should always be true... 931 if (type == SyncManager.SYNC_PING) { 932 int changeCount = SyncManager.getStatusChangeCount(status); 933 if (changeCount > 0) { 934 errorMap.remove(serverId); 935 } else if (changeCount == 0) { 936 // This means that a ping reported changes in error; we keep a count 937 // of consecutive errors of this kind 938 String name = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN); 939 Integer failures = errorMap.get(serverId); 940 if (failures == null) { 941 userLog("Last ping reported changes in error for: ", name); 942 errorMap.put(serverId, 1); 943 } else if (failures > MAX_PING_FAILURES) { 944 // We'll back off of push for this box 945 pushFallback(c.getLong(Mailbox.CONTENT_ID_COLUMN)); 946 continue; 947 } else { 948 userLog("Last ping reported changes in error for: ", name); 949 errorMap.put(serverId, failures + 1); 950 } 951 } 952 } 953 954 // If there were no problems with previous sync, we'll start another one 955 SyncManager.startManualSync(c.getLong(Mailbox.CONTENT_ID_COLUMN), 956 SyncManager.SYNC_PING, null); 957 } 958 } finally { 959 c.close(); 960 } 961 } 962 } 963 return pp.getSyncStatus(); 964 } 965 966 private String getFilterType() { 967 String filter = Eas.FILTER_1_WEEK; 968 switch (mAccount.mSyncLookback) { 969 case com.android.email.Account.SYNC_WINDOW_1_DAY: { 970 filter = Eas.FILTER_1_DAY; 971 break; 972 } 973 case com.android.email.Account.SYNC_WINDOW_3_DAYS: { 974 filter = Eas.FILTER_3_DAYS; 975 break; 976 } 977 case com.android.email.Account.SYNC_WINDOW_1_WEEK: { 978 filter = Eas.FILTER_1_WEEK; 979 break; 980 } 981 case com.android.email.Account.SYNC_WINDOW_2_WEEKS: { 982 filter = Eas.FILTER_2_WEEKS; 983 break; 984 } 985 case com.android.email.Account.SYNC_WINDOW_1_MONTH: { 986 filter = Eas.FILTER_1_MONTH; 987 break; 988 } 989 case com.android.email.Account.SYNC_WINDOW_ALL: { 990 filter = Eas.FILTER_ALL; 991 break; 992 } 993 } 994 return filter; 995 } 996 997 /** 998 * Common code to sync E+PIM data 999 * 1000 * @param target, an EasMailbox, EasContacts, or EasCalendar object 1001 */ 1002 public void sync(AbstractSyncAdapter target) throws IOException { 1003 Mailbox mailbox = target.mMailbox; 1004 1005 boolean moreAvailable = true; 1006 while (!mStop && moreAvailable) { 1007 // If we have no connectivity, just exit cleanly. SyncManager will start us up again 1008 // when connectivity has returned 1009 if (!hasConnectivity()) { 1010 userLog("No connectivity in sync; finishing sync"); 1011 mExitStatus = EXIT_DONE; 1012 return; 1013 } 1014 1015 while (true) { 1016 PartRequest req = null; 1017 synchronized (mPartRequests) { 1018 if (mPartRequests.isEmpty()) { 1019 break; 1020 } else { 1021 req = mPartRequests.get(0); 1022 } 1023 } 1024 getAttachment(req); 1025 synchronized(mPartRequests) { 1026 mPartRequests.remove(req); 1027 } 1028 } 1029 1030 Serializer s = new Serializer(); 1031 String className = target.getCollectionName(); 1032 String syncKey = target.getSyncKey(); 1033 userLog("sync, sending ", className, " syncKey: ", syncKey); 1034 s.start(Tags.SYNC_SYNC) 1035 .start(Tags.SYNC_COLLECTIONS) 1036 .start(Tags.SYNC_COLLECTION) 1037 .data(Tags.SYNC_CLASS, className) 1038 .data(Tags.SYNC_SYNC_KEY, syncKey) 1039 .data(Tags.SYNC_COLLECTION_ID, mailbox.mServerId) 1040 .tag(Tags.SYNC_DELETES_AS_MOVES); 1041 1042 // EAS doesn't like GetChanges if the syncKey is "0"; not documented 1043 if (!syncKey.equals("0")) { 1044 s.tag(Tags.SYNC_GET_CHANGES); 1045 } 1046 s.data(Tags.SYNC_WINDOW_SIZE, 1047 className.equals("Email") ? EMAIL_WINDOW_SIZE : PIM_WINDOW_SIZE); 1048 1049 // Handle options 1050 s.start(Tags.SYNC_OPTIONS); 1051 // Set the lookback appropriately (EAS calls this a "filter") for all but Contacts 1052 if (!className.equals("Contacts")) { 1053 s.data(Tags.SYNC_FILTER_TYPE, getFilterType()); 1054 } 1055 // Set the truncation amount for all classes 1056 if (mProtocolVersionDouble >= 12.0) { 1057 s.start(Tags.BASE_BODY_PREFERENCE) 1058 // HTML for email; plain text for everything else 1059 .data(Tags.BASE_TYPE, (className.equals("Email") ? Eas.BODY_PREFERENCE_HTML 1060 : Eas.BODY_PREFERENCE_TEXT)) 1061 .data(Tags.BASE_TRUNCATION_SIZE, Eas.EAS12_TRUNCATION_SIZE) 1062 .end(); 1063 } else { 1064 s.data(Tags.SYNC_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE); 1065 } 1066 s.end(); 1067 1068 // Send our changes up to the server 1069 target.sendLocalChanges(s); 1070 1071 s.end().end().end().done(); 1072 HttpResponse resp = sendHttpClientPost("Sync", s.toByteArray()); 1073 int code = resp.getStatusLine().getStatusCode(); 1074 if (code == HttpStatus.SC_OK) { 1075 InputStream is = resp.getEntity().getContent(); 1076 if (is != null) { 1077 moreAvailable = target.parse(is); 1078 target.cleanup(); 1079 } else { 1080 userLog("Empty input stream in sync command response"); 1081 } 1082 } else { 1083 userLog("Sync response error: ", code); 1084 if (isAuthError(code)) { 1085 mExitStatus = EXIT_LOGIN_FAILURE; 1086 } else { 1087 mExitStatus = EXIT_IO_ERROR; 1088 } 1089 return; 1090 } 1091 } 1092 mExitStatus = EXIT_DONE; 1093 } 1094 1095 protected boolean setupService() { 1096 // Make sure account and mailbox are always the latest from the database 1097 mAccount = Account.restoreAccountWithId(mContext, mAccount.mId); 1098 if (mAccount == null) return false; 1099 mMailbox = Mailbox.restoreMailboxWithId(mContext, mMailbox.mId); 1100 if (mMailbox == null) return false; 1101 mThread = Thread.currentThread(); 1102 android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND); 1103 TAG = mThread.getName(); 1104 1105 HostAuth ha = HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv); 1106 if (ha == null) return false; 1107 mHostAddress = ha.mAddress; 1108 mUserName = ha.mLogin; 1109 mPassword = ha.mPassword; 1110 1111 // Set up our protocol version from the Account 1112 mProtocolVersion = mAccount.mProtocolVersion; 1113 // If it hasn't been set up, start with default version 1114 if (mProtocolVersion == null) { 1115 mProtocolVersion = DEFAULT_PROTOCOL_VERSION; 1116 } 1117 mProtocolVersionDouble = Double.parseDouble(mProtocolVersion); 1118 return true; 1119 } 1120 1121 /* (non-Javadoc) 1122 * @see java.lang.Runnable#run() 1123 */ 1124 public void run() { 1125 if (!setupService()) return; 1126 1127 try { 1128 SyncManager.callback().syncMailboxStatus(mMailboxId, EmailServiceStatus.IN_PROGRESS, 0); 1129 } catch (RemoteException e1) { 1130 // Don't care if this fails 1131 } 1132 1133 // Whether or not we're the account mailbox 1134 try { 1135 mDeviceId = SyncManager.getDeviceId(); 1136 if ((mMailbox == null) || (mAccount == null)) { 1137 return; 1138 } else if (mMailbox.mType == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) { 1139 runAccountMailbox(); 1140 } else { 1141 AbstractSyncAdapter target; 1142 if (mMailbox.mType == Mailbox.TYPE_CONTACTS) { 1143 target = new ContactsSyncAdapter(mMailbox, this); 1144 } else { 1145 target = new EmailSyncAdapter(mMailbox, this); 1146 } 1147 // We loop here because someone might have put a request in while we were syncing 1148 // and we've missed that opportunity... 1149 do { 1150 if (mRequestTime != 0) { 1151 userLog("Looping for user request..."); 1152 mRequestTime = 0; 1153 } 1154 sync(target); 1155 } while (mRequestTime != 0); 1156 } 1157 } catch (IOException e) { 1158 String message = e.getMessage(); 1159 userLog("Caught IOException: ", ((message == null) ? "No message" : message)); 1160 mExitStatus = EXIT_IO_ERROR; 1161 } catch (Exception e) { 1162 userLog("Uncaught exception in EasSyncService", e); 1163 } finally { 1164 int status; 1165 1166 if (!mStop) { 1167 userLog("Sync finished"); 1168 SyncManager.done(this); 1169 switch (mExitStatus) { 1170 case EXIT_IO_ERROR: 1171 status = EmailServiceStatus.CONNECTION_ERROR; 1172 break; 1173 case EXIT_DONE: 1174 status = EmailServiceStatus.SUCCESS; 1175 ContentValues cv = new ContentValues(); 1176 cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis()); 1177 String s = "S" + mSyncReason + ':' + status + ':' + mChangeCount; 1178 cv.put(Mailbox.SYNC_STATUS, s); 1179 mContentResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, 1180 mMailboxId), cv, null, null); 1181 break; 1182 case EXIT_LOGIN_FAILURE: 1183 status = EmailServiceStatus.LOGIN_FAILED; 1184 break; 1185 default: 1186 status = EmailServiceStatus.REMOTE_EXCEPTION; 1187 errorLog("Sync ended due to an exception."); 1188 break; 1189 } 1190 } else { 1191 userLog("Stopped sync finished."); 1192 status = EmailServiceStatus.SUCCESS; 1193 } 1194 1195 try { 1196 SyncManager.callback().syncMailboxStatus(mMailboxId, status, 0); 1197 } catch (RemoteException e1) { 1198 // Don't care if this fails 1199 } 1200 1201 // Make sure SyncManager knows about this 1202 SyncManager.kick("sync finished"); 1203 } 1204 } 1205} 1206