EasSyncService.java revision f708e075473f4c186c44b61bc5ad5c73c901b61e
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.R; 21import com.android.email.activity.AccountFolderList; 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.exchange.adapter.AbstractSyncAdapter; 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; 40import com.android.exchange.utility.Base64; 41 42import org.apache.http.Header; 43import org.apache.http.HttpEntity; 44import org.apache.http.HttpResponse; 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.entity.ByteArrayEntity; 50import org.apache.http.impl.client.DefaultHttpClient; 51import org.apache.http.params.BasicHttpParams; 52import org.apache.http.params.HttpConnectionParams; 53import org.apache.http.params.HttpParams; 54 55import android.app.Notification; 56import android.app.NotificationManager; 57import android.app.PendingIntent; 58import android.content.ContentResolver; 59import android.content.ContentUris; 60import android.content.ContentValues; 61import android.content.Context; 62import android.content.Intent; 63import android.database.Cursor; 64import android.os.RemoteException; 65 66import java.io.ByteArrayInputStream; 67import java.io.File; 68import java.io.FileOutputStream; 69import java.io.IOException; 70import java.io.InputStream; 71import java.net.HttpURLConnection; 72import java.net.URI; 73import java.net.URLEncoder; 74import java.util.ArrayList; 75import java.util.HashMap; 76 77public class EasSyncService extends AbstractSyncService { 78 79 private static final String EMAIL_WINDOW_SIZE = "5"; 80 public static final String PIM_WINDOW_SIZE = "20"; 81 private static final String WHERE_ACCOUNT_KEY_AND_SERVER_ID = 82 MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SERVER_ID + "=?"; 83 private static final String WHERE_ACCOUNT_AND_SYNC_INTERVAL_PING = 84 MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SYNC_INTERVAL + 85 '=' + Account.CHECK_INTERVAL_PING; 86 private static final String AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX = " AND " + 87 MailboxColumns.SYNC_INTERVAL + " IN (" + Account.CHECK_INTERVAL_PING + 88 ',' + Account.CHECK_INTERVAL_PUSH + ") AND " + MailboxColumns.TYPE + "!=\"" + 89 Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + '\"'; 90 private static final String WHERE_PUSH_HOLD_NOT_ACCOUNT_MAILBOX = 91 MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SYNC_INTERVAL + 92 '=' + Account.CHECK_INTERVAL_PUSH_HOLD; 93 94 static private final int CHUNK_SIZE = 16*1024; 95 96 static private final String PING_COMMAND = "Ping"; 97 static private final int COMMAND_TIMEOUT = 20*SECS; 98 static private final int PING_COMMAND_TIMEOUT = 20*MINS; 99 100 // Reasonable default 101 String mProtocolVersion = "2.5"; 102 public Double mProtocolVersionDouble; 103 private String mDeviceId = null; 104 private String mDeviceType = "Android"; 105 AbstractSyncAdapter mTarget; 106 String mAuthString = null; 107 String mCmdString = null; 108 public String mHostAddress; 109 public String mUserName; 110 public String mPassword; 111 String mDomain = null; 112 boolean mSentCommands; 113 boolean mIsIdle = false; 114 boolean mSsl = true; 115 public Context mContext; 116 public ContentResolver mContentResolver; 117 String[] mBindArguments = new String[2]; 118 InputStream mPendingPartInputStream = null; 119 private boolean mTriedReloadFolderList = false; 120 121 public EasSyncService(Context _context, Mailbox _mailbox) { 122 super(_context, _mailbox); 123 mContext = _context; 124 mContentResolver = _context.getContentResolver(); 125 HostAuth ha = HostAuth.restoreHostAuthWithId(_context, mAccount.mHostAuthKeyRecv); 126 mSsl = (ha.mFlags & HostAuth.FLAG_SSL) != 0; 127 } 128 129 private EasSyncService(String prefix) { 130 super(prefix); 131 } 132 133 public EasSyncService() { 134 this("EAS Validation"); 135 } 136 137 @Override 138 public void ping() { 139 userLog("We've been pinged!"); 140 Object synchronizer = getSynchronizer(); 141 synchronized (synchronizer) { 142 synchronizer.notify(); 143 } 144 } 145 146 @Override 147 public void stop() { 148 mStop = true; 149 } 150 151 @Override 152 public int getSyncStatus() { 153 return 0; 154 } 155 156 private boolean isAuthError(int code) { 157 return (code == HttpURLConnection.HTTP_UNAUTHORIZED || code == HttpURLConnection.HTTP_FORBIDDEN 158 || code == HttpURLConnection.HTTP_INTERNAL_ERROR); 159 } 160 161 /* (non-Javadoc) 162 * @see com.android.exchange.SyncService#validateAccount(java.lang.String, java.lang.String, java.lang.String, int, boolean, android.content.Context) 163 */ 164 @Override 165 public void validateAccount(String hostAddress, String userName, String password, int port, 166 boolean ssl, Context context) throws MessagingException { 167 try { 168 userLog("Testing EAS: " + hostAddress + ", " + userName + ", ssl = " + ssl); 169 EasSyncService svc = new EasSyncService("%TestAccount%"); 170 svc.mContext = context; 171 svc.mHostAddress = hostAddress; 172 svc.mUserName = userName; 173 svc.mPassword = password; 174 svc.mSsl = ssl; 175 svc.mDeviceId = SyncManager.getDeviceId(); 176 HttpResponse resp = svc.sendHttpClientOptions(); 177 int code = resp.getStatusLine().getStatusCode(); 178 userLog("Validation (OPTIONS) response: " + code); 179 if (code == HttpURLConnection.HTTP_OK) { 180 // No exception means successful validation 181 userLog("Validation successful"); 182 return; 183 } 184 if (isAuthError(code)) { 185 userLog("Authentication failed"); 186 throw new AuthenticationFailedException("Validation failed"); 187 } else { 188 // TODO Need to catch other kinds of errors (e.g. policy) For now, report the code. 189 userLog("Validation failed, reporting I/O error: " + code); 190 throw new MessagingException(MessagingException.IOERROR); 191 } 192 } catch (IOException e) { 193 userLog("IOException caught, reporting I/O error: " + e.getMessage()); 194 throw new MessagingException(MessagingException.IOERROR); 195 } 196 197 } 198 199 private void doStatusCallback(long messageId, long attachmentId, int status) { 200 try { 201 SyncManager.callback().loadAttachmentStatus(messageId, attachmentId, status, 0); 202 } catch (RemoteException e) { 203 // No danger if the client is no longer around 204 } 205 } 206 207 private void doProgressCallback(long messageId, long attachmentId, int progress) { 208 try { 209 SyncManager.callback().loadAttachmentStatus(messageId, attachmentId, 210 EmailServiceStatus.IN_PROGRESS, progress); 211 } catch (RemoteException e) { 212 // No danger if the client is no longer around 213 } 214 } 215 216 public File createUniqueFileInternal(String dir, String filename) { 217 File directory; 218 if (dir == null) { 219 directory = mContext.getFilesDir(); 220 } else { 221 directory = new File(dir); 222 } 223 if (!directory.exists()) { 224 directory.mkdirs(); 225 } 226 File file = new File(directory, filename); 227 if (!file.exists()) { 228 return file; 229 } 230 // Get the extension of the file, if any. 231 int index = filename.lastIndexOf('.'); 232 String name = filename; 233 String extension = ""; 234 if (index != -1) { 235 name = filename.substring(0, index); 236 extension = filename.substring(index); 237 } 238 for (int i = 2; i < Integer.MAX_VALUE; i++) { 239 file = new File(directory, name + '-' + i + extension); 240 if (!file.exists()) { 241 return file; 242 } 243 } 244 return null; 245 } 246 247 /** 248 * Loads an attachment, based on the PartRequest passed in. The PartRequest is basically our 249 * wrapper for Attachment 250 * @param req the part (attachment) to be retrieved 251 * @throws IOException 252 */ 253 protected void getAttachment(PartRequest req) throws IOException { 254 Attachment att = req.att; 255 Message msg = Message.restoreMessageWithId(mContext, att.mMessageKey); 256 doProgressCallback(msg.mId, att.mId, 0); 257 DefaultHttpClient client = new DefaultHttpClient(); 258 String us = makeUriString("GetAttachment", "&AttachmentName=" + att.mLocation); 259 HttpPost method = new HttpPost(URI.create(us)); 260 method.setHeader("Authorization", mAuthString); 261 262 HttpResponse res = client.execute(method); 263 int status = res.getStatusLine().getStatusCode(); 264 if (status == HttpURLConnection.HTTP_OK) { 265 HttpEntity e = res.getEntity(); 266 int len = (int)e.getContentLength(); 267 String type = e.getContentType().getValue(); 268 InputStream is = res.getEntity().getContent(); 269 File f = (req.destination != null) 270 ? new File(req.destination) 271 : createUniqueFileInternal(req.destination, att.mFileName); 272 if (f != null) { 273 // Ensure that the target directory exists 274 File destDir = f.getParentFile(); 275 if (!destDir.exists()) { 276 destDir.mkdirs(); 277 } 278 FileOutputStream os = new FileOutputStream(f); 279 if (len > 0) { 280 try { 281 mPendingPartRequest = req; 282 mPendingPartInputStream = is; 283 byte[] bytes = new byte[CHUNK_SIZE]; 284 int length = len; 285 while (len > 0) { 286 int n = (len > CHUNK_SIZE ? CHUNK_SIZE : len); 287 int read = is.read(bytes, 0, n); 288 os.write(bytes, 0, read); 289 len -= read; 290 int pct = ((length - len) * 100 / length); 291 doProgressCallback(msg.mId, att.mId, pct); 292 } 293 } finally { 294 mPendingPartRequest = null; 295 mPendingPartInputStream = null; 296 } 297 } 298 os.flush(); 299 os.close(); 300 301 // EmailProvider will throw an exception if we try to update an unsaved attachment 302 if (att.isSaved()) { 303 String contentUriString = (req.contentUriString != null) 304 ? req.contentUriString 305 : "file://" + f.getAbsolutePath(); 306 ContentValues cv = new ContentValues(); 307 cv.put(AttachmentColumns.CONTENT_URI, contentUriString); 308 cv.put(AttachmentColumns.MIME_TYPE, type); 309 att.update(mContext, cv); 310 doStatusCallback(msg.mId, att.mId, EmailServiceStatus.SUCCESS); 311 } 312 } 313 } else { 314 doStatusCallback(msg.mId, att.mId, EmailServiceStatus.MESSAGE_NOT_FOUND); 315 } 316 } 317 318 @SuppressWarnings("deprecation") 319 private String makeUriString(String cmd, String extra) throws IOException { 320 // Cache the authentication string and the command string 321 String safeUserName = URLEncoder.encode(mUserName); 322 if (mAuthString == null) { 323 String cs = mUserName + ':' + mPassword; 324 mAuthString = "Basic " + Base64.encodeBytes(cs.getBytes()); 325 mCmdString = "&User=" + safeUserName + "&DeviceId=" + mDeviceId + "&DeviceType=" 326 + mDeviceType; 327 } 328 String us = (mSsl ? "https" : "http") + "://" + mHostAddress + 329 "/Microsoft-Server-ActiveSync"; 330 if (cmd != null) { 331 us += "?Cmd=" + cmd + mCmdString; 332 } 333 if (extra != null) { 334 us += extra; 335 } 336 return us; 337 } 338 339 private void setHeaders(HttpRequestBase method) { 340 method.setHeader("Authorization", mAuthString); 341 method.setHeader("MS-ASProtocolVersion", mProtocolVersion); 342 method.setHeader("Connection", "keep-alive"); 343 method.setHeader("User-Agent", mDeviceType + '/' + Eas.VERSION); 344 } 345 346 private HttpClient getHttpClient(int timeout) { 347 HttpParams params = new BasicHttpParams(); 348 HttpConnectionParams.setConnectionTimeout(params, 10*SECS); 349 HttpConnectionParams.setSoTimeout(params, timeout); 350 return new DefaultHttpClient(params); 351 } 352 353 protected HttpResponse sendHttpClientPost(String cmd, byte[] bytes) throws IOException { 354 HttpClient client = 355 getHttpClient(cmd.equals(PING_COMMAND) ? PING_COMMAND_TIMEOUT : COMMAND_TIMEOUT); 356 String us = makeUriString(cmd, null); 357 HttpPost method = new HttpPost(URI.create(us)); 358 if (cmd.startsWith("SendMail&")) { 359 method.setHeader("Content-Type", "message/rfc822"); 360 } else { 361 method.setHeader("Content-Type", "application/vnd.ms-sync.wbxml"); 362 } 363 setHeaders(method); 364 method.setEntity(new ByteArrayEntity(bytes)); 365 return client.execute(method); 366 } 367 368 protected HttpResponse sendHttpClientOptions() throws IOException { 369 HttpClient client = getHttpClient(COMMAND_TIMEOUT); 370 String us = makeUriString("OPTIONS", null); 371 HttpOptions method = new HttpOptions(URI.create(us)); 372 setHeaders(method); 373 return client.execute(method); 374 } 375 376 String getTargetCollectionClassFromCursor(Cursor c) { 377 int type = c.getInt(Mailbox.CONTENT_TYPE_COLUMN); 378 if (type == Mailbox.TYPE_CONTACTS) { 379 return "Contacts"; 380 } else if (type == Mailbox.TYPE_CALENDAR) { 381 return "Calendar"; 382 } else { 383 return "Email"; 384 } 385 } 386 387 /** 388 * Performs FolderSync 389 * 390 * @throws IOException 391 * @throws EasParserException 392 */ 393 public void runAccountMailbox() throws IOException, EasParserException { 394 // Initialize exit status to success 395 mExitStatus = EmailServiceStatus.SUCCESS; 396 try { 397 try { 398 SyncManager.callback() 399 .syncMailboxListStatus(mAccount.mId, EmailServiceStatus.IN_PROGRESS, 0); 400 } catch (RemoteException e1) { 401 // Don't care if this fails 402 } 403 404 if (mAccount.mSyncKey == null) { 405 mAccount.mSyncKey = "0"; 406 userLog("Account syncKey INIT to 0"); 407 ContentValues cv = new ContentValues(); 408 cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey); 409 mAccount.update(mContext, cv); 410 } 411 412 boolean firstSync = mAccount.mSyncKey.equals("0"); 413 if (firstSync) { 414 userLog("Initial FolderSync"); 415 } 416 417 // When we first start up, change all ping mailboxes to push. 418 ContentValues cv = new ContentValues(); 419 cv.put(Mailbox.SYNC_INTERVAL, Account.CHECK_INTERVAL_PUSH); 420 if (mContentResolver.update(Mailbox.CONTENT_URI, cv, 421 WHERE_ACCOUNT_AND_SYNC_INTERVAL_PING, 422 new String[] {Long.toString(mAccount.mId)}) > 0) { 423 SyncManager.kick("change ping boxes to push"); 424 } 425 426 // Determine our protocol version, if we haven't already 427 if (mAccount.mProtocolVersion == null) { 428 userLog("Determine EAS protocol version"); 429 HttpResponse resp = sendHttpClientOptions(); 430 int code = resp.getStatusLine().getStatusCode(); 431 userLog("OPTIONS response: " + code); 432 if (code == HttpURLConnection.HTTP_OK) { 433 Header header = resp.getFirstHeader("ms-asprotocolversions"); 434 String versions = header.getValue(); 435 if (versions != null) { 436 if (versions.contains("12.0")) { 437 mProtocolVersion = "12.0"; 438 } 439 mProtocolVersionDouble = Double.parseDouble(mProtocolVersion); 440 mAccount.mProtocolVersion = mProtocolVersion; 441 userLog(versions); 442 userLog("Using version " + mProtocolVersion); 443 } else { 444 errorLog("No protocol versions in OPTIONS response"); 445 throw new IOException(); 446 } 447 } else { 448 errorLog("OPTIONS command failed; throwing IOException"); 449 throw new IOException(); 450 } 451 } 452 453 while (!mStop) { 454 userLog("Sending Account syncKey: " + mAccount.mSyncKey); 455 Serializer s = new Serializer(); 456 s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY) 457 .text(mAccount.mSyncKey).end().end().done(); 458 HttpResponse resp = sendHttpClientPost("FolderSync", s.toByteArray()); 459 if (mStop) break; 460 int code = resp.getStatusLine().getStatusCode(); 461 if (code == HttpURLConnection.HTTP_OK) { 462 HttpEntity entity = resp.getEntity(); 463 int len = (int)entity.getContentLength(); 464 if (len > 0) { 465 InputStream is = entity.getContent(); 466 // Returns true if we need to sync again 467 userLog("FolderSync, deviceId = " + mDeviceId); 468 if (new FolderSyncParser(is, this).parse()) { 469 continue; 470 } 471 } 472 } else if (code == HttpURLConnection.HTTP_UNAUTHORIZED || 473 code == HttpURLConnection.HTTP_FORBIDDEN) { 474 mExitStatus = AbstractSyncService.EXIT_LOGIN_FAILURE; 475 } else { 476 userLog("FolderSync response error: " + code); 477 } 478 479 // Change all push/hold boxes to push 480 cv = new ContentValues(); 481 cv.put(Mailbox.SYNC_INTERVAL, Account.CHECK_INTERVAL_PUSH); 482 if (mContentResolver.update(Mailbox.CONTENT_URI, cv, 483 WHERE_PUSH_HOLD_NOT_ACCOUNT_MAILBOX, 484 new String[] {Long.toString(mAccount.mId)}) > 0) { 485 userLog("Set push/hold boxes to push..."); 486 } 487 488 try { 489 SyncManager.callback() 490 .syncMailboxListStatus(mAccount.mId, mExitStatus, 0); 491 } catch (RemoteException e1) { 492 // Don't care if this fails 493 } 494 495 // Wait for push notifications. 496 String threadName = Thread.currentThread().getName(); 497 try { 498 runPingLoop(); 499 } catch (StaleFolderListException e) { 500 // We break out if we get told about a stale folder list 501 userLog("Ping interrupted; folder list requires sync..."); 502 } finally { 503 Thread.currentThread().setName(threadName); 504 } 505 } 506 } catch (IOException e) { 507 // We catch this here to send the folder sync status callback 508 // A folder sync failed callback will get sent from run() 509 try { 510 if (!mStop) { 511 SyncManager.callback() 512 .syncMailboxListStatus(mAccount.mId, 513 EmailServiceStatus.CONNECTION_ERROR, 0); 514 } 515 } catch (RemoteException e1) { 516 // Don't care if this fails 517 } 518 throw new IOException(); 519 } 520 } 521 522 void pushFallback() { 523 // We'll try reloading folders first; this has been observed to work in some cases 524 if (!mTriedReloadFolderList) { 525 errorLog("*** PING LOOP: Trying to reload folder list..."); 526 SyncManager.reloadFolderList(mContext, mAccount.mId, true); 527 mTriedReloadFolderList = true; 528 // If we've tried that, set all mailboxes (except the account mailbox) to 5 minute sync 529 } else { 530 errorLog("*** PING LOOP: Turning off push due to ping loop..."); 531 ContentValues cv = new ContentValues(); 532 cv.put(Mailbox.SYNC_INTERVAL, 5); 533 mContentResolver.update(Mailbox.CONTENT_URI, cv, 534 MailboxColumns.ACCOUNT_KEY + '=' + mAccount.mId 535 + AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX, null); 536 // Now, change the account as well 537 cv.clear(); 538 cv.put(Account.SYNC_INTERVAL, 5); 539 mContentResolver.update(ContentUris.withAppendedId(Account.CONTENT_URI, mAccount.mId), 540 cv, null, null); 541 // TODO Discuss the best way to alert the user 542 // Alert the user about what we've done 543 NotificationManager nm = (NotificationManager)mContext 544 .getSystemService(Context.NOTIFICATION_SERVICE); 545 Notification note = 546 new Notification(R.drawable.stat_notify_email_generic, 547 mContext.getString(R.string.notification_ping_loop_title), 548 System.currentTimeMillis()); 549 Intent i = new Intent(mContext, AccountFolderList.class); 550 PendingIntent pi = PendingIntent.getActivity(mContext, 0, i, 0); 551 note.setLatestEventInfo(mContext, 552 mContext.getString(R.string.notification_ping_loop_title), 553 mContext.getString(R.string.notification_ping_loop_text), pi); 554 nm.notify(Eas.EXCHANGE_ERROR_NOTIFICATION, note); 555 } 556 } 557 558 void runPingLoop() throws IOException, StaleFolderListException { 559 // Do push for all sync services here 560 ArrayList<Mailbox> pushBoxes = new ArrayList<Mailbox>(); 561 long endTime = System.currentTimeMillis() + (30*MINS); 562 HashMap<Long, Integer> pingFailureMap = new HashMap<Long, Integer>(); 563 564 while (System.currentTimeMillis() < endTime) { 565 // Count of pushable mailboxes 566 int pushCount = 0; 567 // Count of mailboxes that can be pushed right now 568 int canPushCount = 0; 569 Serializer s = new Serializer(); 570 int code; 571 Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, 572 MailboxColumns.ACCOUNT_KEY + '=' + mAccount.mId + 573 AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX, null, null); 574 575 pushBoxes.clear(); 576 577 try { 578 // Loop through our pushed boxes seeing what is available to push 579 while (c.moveToNext()) { 580 pushCount++; 581 // Two requirements for push: 582 // 1) SyncManager tells us the mailbox is syncable (not running, not stopped) 583 // 2) The syncKey isn't "0" (i.e. it's synced at least once) 584 long mailboxId = c.getLong(Mailbox.CONTENT_ID_COLUMN); 585 if (SyncManager.canSync(mailboxId)) { 586 String syncKey = c.getString(Mailbox.CONTENT_SYNC_KEY_COLUMN); 587 if (syncKey == null || syncKey.equals("0")) { 588 continue; 589 } 590 591 // Take a peek at this box's behavior last sync 592 // We do this because some Exchange 2003 servers put themselves (and 593 // therefore our client) into a "ping loop" in which the client is 594 // continuously told of server changes, only to find that there aren't any. 595 // This behavior is seemingly random, and we must code defensively by 596 // backing off of push behavior when this is detected. 597 // The server fix is at http://support.microsoft.com/kb/923282 598 599 // Sync status is encoded as S<type>:<exitstatus>:<changes> 600 String status = c.getString(Mailbox.CONTENT_SYNC_STATUS_COLUMN); 601 int type = SyncManager.getStatusType(status); 602 if (type == SyncManager.SYNC_PING) { 603 int changeCount = SyncManager.getStatusChangeCount(status); 604 if (changeCount == 0) { 605 // This means that a ping failed; we'll keep track of this 606 Integer failures = pingFailureMap.get(mailboxId); 607 if (failures == null) { 608 pingFailureMap.put(mailboxId, 1); 609 } else if (failures > 4) { 610 // Change all push/ping boxes (except account) to 5 minute sync 611 pushFallback(); 612 return; 613 } else { 614 pingFailureMap.put(mailboxId, failures + 1); 615 } 616 } else { 617 pingFailureMap.put(mailboxId, 0); 618 } 619 } 620 621 if (canPushCount++ == 0) { 622 // Initialize the Ping command 623 s.start(Tags.PING_PING).data(Tags.PING_HEARTBEAT_INTERVAL, "900") 624 .start(Tags.PING_FOLDERS); 625 } 626 // When we're ready for Calendar/Contacts, we will check folder type 627 // TODO Save Calendar and Contacts!! Mark as not visible! 628 String folderClass = getTargetCollectionClassFromCursor(c); 629 s.start(Tags.PING_FOLDER) 630 .data(Tags.PING_ID, c.getString(Mailbox.CONTENT_SERVER_ID_COLUMN)) 631 .data(Tags.PING_CLASS, folderClass) 632 .end(); 633 userLog("Ping ready for: " + folderClass + ", " + 634 c.getString(Mailbox.CONTENT_SERVER_ID_COLUMN) + " (" + 635 c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN) + ')'); 636 pushBoxes.add(new Mailbox().restore(c)); 637 } else { 638 userLog(c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN) + 639 " not ready for ping"); 640 } 641 } 642 } finally { 643 c.close(); 644 } 645 646 if (canPushCount > 0 && (canPushCount == pushCount)) { 647 // If we have some number that are ready for push, send Ping to the server 648 s.end().end().done(); 649 650 Thread.currentThread().setName(mAccount.mDisplayName + ": Ping"); 651 userLog("Sending ping, timeout: " + PING_COMMAND_TIMEOUT / MINS + "m"); 652 653 SyncManager.runAsleep(mMailboxId, PING_COMMAND_TIMEOUT); 654 HttpResponse res = sendHttpClientPost(PING_COMMAND, s.toByteArray()); 655 SyncManager.runAwake(mMailboxId); 656 657 // Don't send request if we've been asked to stop 658 if (mStop) return; 659 long time = System.currentTimeMillis(); 660 code = res.getStatusLine().getStatusCode(); 661 662 // Return immediately if we've been asked to stop 663 if (mStop) { 664 userLog("Stopping pingLoop"); 665 return; 666 } 667 668 // Get elapsed time 669 time = System.currentTimeMillis() - time; 670 userLog("Ping response: " + code + " in " + time + "ms"); 671 672 if (code == HttpURLConnection.HTTP_OK) { 673 HttpEntity e = res.getEntity(); 674 int len = (int)e.getContentLength(); 675 InputStream is = res.getEntity().getContent(); 676 if (len > 0) { 677 parsePingResult(is, mContentResolver); 678 } else { 679 throw new IOException(); 680 } 681 } else if (isAuthError(code)) { 682 mExitStatus = AbstractSyncService.EXIT_LOGIN_FAILURE; 683 userLog("Authorization error during Ping: " + code); 684 throw new IOException(); 685 } 686 } else if (pushCount > 0) { 687 // If we want to Ping, but can't just yet, wait 10 seconds and try again 688 userLog("pingLoop waiting for " + (pushCount - canPushCount) + " box(es)"); 689 sleep(10*SECS); 690 } else { 691 // We've got nothing to do, so let's hang out for a while 692 sleep(20*MINS); 693 } 694 } 695 } 696 697 void sleep(long ms) { 698 try { 699 Thread.sleep(ms); 700 } catch (InterruptedException e) { 701 // Doesn't matter whether we stop early; it's the thought that counts 702 } 703 } 704 705 private int parsePingResult(InputStream is, ContentResolver cr) 706 throws IOException, StaleFolderListException { 707 PingParser pp = new PingParser(is, this); 708 if (pp.parse()) { 709 // True indicates some mailboxes need syncing... 710 // syncList has the serverId's of the mailboxes... 711 mBindArguments[0] = Long.toString(mAccount.mId); 712 ArrayList<String> syncList = pp.getSyncList(); 713 for (String serverId: syncList) { 714 mBindArguments[1] = serverId; 715 Cursor c = cr.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, 716 WHERE_ACCOUNT_KEY_AND_SERVER_ID, mBindArguments, null); 717 try { 718 if (c.moveToFirst()) { 719 SyncManager.startManualSync(c.getLong(Mailbox.CONTENT_ID_COLUMN), 720 SyncManager.SYNC_PING, null); 721 } 722 } finally { 723 c.close(); 724 } 725 } 726 } 727 return pp.getSyncList().size(); 728 } 729 730 ByteArrayInputStream readResponse(HttpURLConnection uc) throws IOException { 731 String encoding = uc.getHeaderField("Transfer-Encoding"); 732 if (encoding == null) { 733 int len = uc.getHeaderFieldInt("Content-Length", 0); 734 if (len > 0) { 735 InputStream in = uc.getInputStream(); 736 byte[] bytes = new byte[len]; 737 int remain = len; 738 int offs = 0; 739 while (remain > 0) { 740 int read = in.read(bytes, offs, remain); 741 remain -= read; 742 offs += read; 743 } 744 return new ByteArrayInputStream(bytes); 745 } 746 } else if (encoding.equalsIgnoreCase("chunked")) { 747 // TODO We don't handle this yet 748 return null; 749 } 750 return null; 751 } 752 753 String readResponseString(HttpURLConnection uc) throws IOException { 754 String encoding = uc.getHeaderField("Transfer-Encoding"); 755 if (encoding == null) { 756 int len = uc.getHeaderFieldInt("Content-Length", 0); 757 if (len > 0) { 758 InputStream in = uc.getInputStream(); 759 byte[] bytes = new byte[len]; 760 int remain = len; 761 int offs = 0; 762 while (remain > 0) { 763 int read = in.read(bytes, offs, remain); 764 remain -= read; 765 offs += read; 766 } 767 return new String(bytes); 768 } 769 } else if (encoding.equalsIgnoreCase("chunked")) { 770 // TODO We don't handle this yet 771 return null; 772 } 773 return null; 774 } 775 776 private String getFilterType() { 777 String filter = Eas.FILTER_1_WEEK; 778 switch (mAccount.mSyncLookback) { 779 case com.android.email.Account.SYNC_WINDOW_1_DAY: { 780 filter = Eas.FILTER_1_DAY; 781 break; 782 } 783 case com.android.email.Account.SYNC_WINDOW_3_DAYS: { 784 filter = Eas.FILTER_3_DAYS; 785 break; 786 } 787 case com.android.email.Account.SYNC_WINDOW_1_WEEK: { 788 filter = Eas.FILTER_1_WEEK; 789 break; 790 } 791 case com.android.email.Account.SYNC_WINDOW_2_WEEKS: { 792 filter = Eas.FILTER_2_WEEKS; 793 break; 794 } 795 case com.android.email.Account.SYNC_WINDOW_1_MONTH: { 796 filter = Eas.FILTER_1_MONTH; 797 break; 798 } 799 case com.android.email.Account.SYNC_WINDOW_ALL: { 800 filter = Eas.FILTER_ALL; 801 break; 802 } 803 } 804 return filter; 805 } 806 807 /** 808 * Common code to sync E+PIM data 809 * 810 * @param target, an EasMailbox, EasContacts, or EasCalendar object 811 */ 812 public void sync(AbstractSyncAdapter target) throws IOException { 813 mTarget = target; 814 Mailbox mailbox = target.mMailbox; 815 816 boolean moreAvailable = true; 817 while (!mStop && moreAvailable) { 818 waitForConnectivity(); 819 820 while (true) { 821 PartRequest req = null; 822 synchronized (mPartRequests) { 823 if (mPartRequests.isEmpty()) { 824 break; 825 } else { 826 req = mPartRequests.get(0); 827 } 828 } 829 getAttachment(req); 830 synchronized(mPartRequests) { 831 mPartRequests.remove(req); 832 } 833 } 834 835 Serializer s = new Serializer(); 836 if (mailbox.mSyncKey == null) { 837 userLog("Mailbox syncKey RESET"); 838 mailbox.mSyncKey = "0"; 839 } 840 String className = target.getCollectionName(); 841 userLog("Sending " + className + " syncKey: " + mailbox.mSyncKey); 842 s.start(Tags.SYNC_SYNC) 843 .start(Tags.SYNC_COLLECTIONS) 844 .start(Tags.SYNC_COLLECTION) 845 .data(Tags.SYNC_CLASS, className) 846 .data(Tags.SYNC_SYNC_KEY, mailbox.mSyncKey) 847 .data(Tags.SYNC_COLLECTION_ID, mailbox.mServerId) 848 .tag(Tags.SYNC_DELETES_AS_MOVES); 849 850 // EAS doesn't like GetChanges if the syncKey is "0"; not documented 851 if (!mailbox.mSyncKey.equals("0")) { 852 s.tag(Tags.SYNC_GET_CHANGES); 853 } 854 s.data(Tags.SYNC_WINDOW_SIZE, 855 className.equals("Email") ? EMAIL_WINDOW_SIZE : PIM_WINDOW_SIZE); 856 boolean options = false; 857 if (!className.equals("Contacts")) { 858 // Set the lookback appropriately (EAS calls this a "filter") 859 s.start(Tags.SYNC_OPTIONS).data(Tags.SYNC_FILTER_TYPE, getFilterType()); 860 // No truncation in this version 861 //if (mProtocolVersionDouble < 12.0) { 862 // s.data(Tags.SYNC_TRUNCATION, "7"); 863 //} 864 options = true; 865 } 866 if (mProtocolVersionDouble >= 12.0) { 867 if (!options) { 868 options = true; 869 s.start(Tags.SYNC_OPTIONS); 870 } 871 s.start(Tags.BASE_BODY_PREFERENCE) 872 // HTML for email; plain text for everything else 873 .data(Tags.BASE_TYPE, (className.equals("Email") ? Eas.BODY_PREFERENCE_HTML 874 : Eas.BODY_PREFERENCE_TEXT)) 875 // No truncation in this version 876 //.data(Tags.BASE_TRUNCATION_SIZE, Eas.DEFAULT_BODY_TRUNCATION_SIZE) 877 .end(); 878 } 879 if (options) { 880 s.end(); 881 } 882 883 // Send our changes up to the server 884 target.sendLocalChanges(s, this); 885 886 s.end().end().end().done(); 887 HttpResponse resp = sendHttpClientPost("Sync", s.toByteArray()); 888 int code = resp.getStatusLine().getStatusCode(); 889 if (code == HttpURLConnection.HTTP_OK) { 890 InputStream is = resp.getEntity().getContent(); 891 if (is != null) { 892 moreAvailable = target.parse(is, this); 893 target.cleanup(this); 894 } 895 } else { 896 userLog("Sync response error: " + code); 897 if (isAuthError(code)) { 898 mExitStatus = AbstractSyncService.EXIT_LOGIN_FAILURE; 899 } 900 return; 901 } 902 } 903 } 904 905 /* (non-Javadoc) 906 * @see java.lang.Runnable#run() 907 */ 908 public void run() { 909 mThread = Thread.currentThread(); 910 TAG = mThread.getName(); 911 912 HostAuth ha = HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv); 913 mHostAddress = ha.mAddress; 914 mUserName = ha.mLogin; 915 mPassword = ha.mPassword; 916 917 try { 918 SyncManager.callback().syncMailboxStatus(mMailboxId, EmailServiceStatus.IN_PROGRESS, 0); 919 } catch (RemoteException e1) { 920 // Don't care if this fails 921 } 922 923 // Make sure account and mailbox are always the latest from the database 924 mAccount = Account.restoreAccountWithId(mContext, mAccount.mId); 925 mMailbox = Mailbox.restoreMailboxWithId(mContext, mMailbox.mId); 926 // Whether or not we're the account mailbox 927 boolean accountMailbox = false; 928 try { 929 mDeviceId = SyncManager.getDeviceId(); 930 if (mMailbox == null || mAccount == null) { 931 return; 932 } else if (mMailbox.mType == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) { 933 accountMailbox = true; 934 runAccountMailbox(); 935 } else { 936 AbstractSyncAdapter target; 937 mAccount = Account.restoreAccountWithId(mContext, mAccount.mId); 938 mProtocolVersion = mAccount.mProtocolVersion; 939 mProtocolVersionDouble = Double.parseDouble(mProtocolVersion); 940 if (mMailbox.mType == Mailbox.TYPE_CONTACTS) 941 target = new ContactsSyncAdapter(mMailbox, this); 942 else { 943 target = new EmailSyncAdapter(mMailbox, this); 944 } 945 // We loop here because someone might have put a request in while we were syncing 946 // and we've missed that opportunity... 947 do { 948 if (mRequestTime != 0) { 949 userLog("Looping for user request..."); 950 mRequestTime = 0; 951 } 952 sync(target); 953 } while (mRequestTime != 0); 954 } 955 mExitStatus = EXIT_DONE; 956 } catch (IOException e) { 957 userLog("Caught IOException"); 958 mExitStatus = EXIT_IO_ERROR; 959 } catch (Exception e) { 960 e.printStackTrace(); 961 } finally { 962 if (!mStop) { 963 userLog(mMailbox.mDisplayName + ": sync finished"); 964 SyncManager.done(this); 965 // If this is the account mailbox, wake up SyncManager 966 // Because this box has a "push" interval, it will be restarted immediately 967 // which will cause the folder list to be reloaded... 968 try { 969 int status; 970 switch (mExitStatus) { 971 case EXIT_IO_ERROR: 972 status = EmailServiceStatus.CONNECTION_ERROR; 973 break; 974 case EXIT_DONE: 975 status = EmailServiceStatus.SUCCESS; 976 break; 977 case EXIT_LOGIN_FAILURE: 978 status = EmailServiceStatus.LOGIN_FAILED; 979 break; 980 default: 981 status = EmailServiceStatus.REMOTE_EXCEPTION; 982 break; 983 } 984 SyncManager.callback().syncMailboxStatus(mMailboxId, status, 0); 985 986 // Save the sync time and status 987 ContentValues cv = new ContentValues(); 988 cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis()); 989 String s = "S" + mSyncReason + ':' + status + ':' + mChangeCount; 990 cv.put(Mailbox.SYNC_STATUS, s); 991 mContentResolver.update(ContentUris 992 .withAppendedId(Mailbox.CONTENT_URI, mMailboxId), cv, null, null); 993 } catch (RemoteException e1) { 994 // Don't care if this fails 995 } 996 } else { 997 userLog(mMailbox.mDisplayName + ": stopped thread finished."); 998 } 999 1000 // Make sure this gets restarted... 1001 if (accountMailbox) { 1002 SyncManager.kick("account mailbox stopped"); 1003 } 1004 } 1005 } 1006} 1007