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