EasSyncService.java revision 8047ef058e41c164c2c8ab230ae8d123f042c167
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 if (new FolderSyncParser(is, this).parse()) { 468 continue; 469 } 470 } 471 } else if (code == HttpURLConnection.HTTP_UNAUTHORIZED || 472 code == HttpURLConnection.HTTP_FORBIDDEN) { 473 mExitStatus = AbstractSyncService.EXIT_LOGIN_FAILURE; 474 } else { 475 userLog("FolderSync response error: " + code); 476 } 477 478 // Change all push/hold boxes to push 479 cv = new ContentValues(); 480 cv.put(Mailbox.SYNC_INTERVAL, Account.CHECK_INTERVAL_PUSH); 481 if (mContentResolver.update(Mailbox.CONTENT_URI, cv, 482 WHERE_PUSH_HOLD_NOT_ACCOUNT_MAILBOX, 483 new String[] {Long.toString(mAccount.mId)}) > 0) { 484 userLog("Set push/hold boxes to push..."); 485 } 486 487 try { 488 SyncManager.callback() 489 .syncMailboxListStatus(mAccount.mId, mExitStatus, 0); 490 } catch (RemoteException e1) { 491 // Don't care if this fails 492 } 493 494 // Wait for push notifications. 495 String threadName = Thread.currentThread().getName(); 496 try { 497 runPingLoop(); 498 } catch (StaleFolderListException e) { 499 // We break out if we get told about a stale folder list 500 userLog("Ping interrupted; folder list requires sync..."); 501 } finally { 502 Thread.currentThread().setName(threadName); 503 } 504 } 505 } catch (IOException e) { 506 // We catch this here to send the folder sync status callback 507 // A folder sync failed callback will get sent from run() 508 try { 509 if (!mStop) { 510 SyncManager.callback() 511 .syncMailboxListStatus(mAccount.mId, 512 EmailServiceStatus.CONNECTION_ERROR, 0); 513 } 514 } catch (RemoteException e1) { 515 // Don't care if this fails 516 } 517 throw new IOException(); 518 } 519 } 520 521 void pushFallback() { 522 // We'll try reloading folders first; this has been observed to work in some cases 523 if (!mTriedReloadFolderList) { 524 errorLog("*** PING LOOP: Trying to reload folder list..."); 525 SyncManager.reloadFolderList(mContext, mAccount.mId, true); 526 mTriedReloadFolderList = true; 527 // If we've tried that, set all mailboxes (except the account mailbox) to 5 minute sync 528 } else { 529 errorLog("*** PING LOOP: Turning off push due to ping loop..."); 530 ContentValues cv = new ContentValues(); 531 cv.put(Mailbox.SYNC_INTERVAL, 5); 532 mContentResolver.update(Mailbox.CONTENT_URI, cv, 533 MailboxColumns.ACCOUNT_KEY + '=' + mAccount.mId 534 + AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX, null); 535 // Now, change the account as well 536 cv.clear(); 537 cv.put(Account.SYNC_INTERVAL, 5); 538 mContentResolver.update(ContentUris.withAppendedId(Account.CONTENT_URI, mAccount.mId), 539 cv, null, null); 540 // TODO Discuss the best way to alert the user 541 // Alert the user about what we've done 542 NotificationManager nm = (NotificationManager)mContext 543 .getSystemService(Context.NOTIFICATION_SERVICE); 544 Notification note = 545 new Notification(R.drawable.stat_notify_email_generic, 546 mContext.getString(R.string.notification_ping_loop_title), 547 System.currentTimeMillis()); 548 Intent i = new Intent(mContext, AccountFolderList.class); 549 PendingIntent pi = PendingIntent.getActivity(mContext, 0, i, 0); 550 note.setLatestEventInfo(mContext, 551 mContext.getString(R.string.notification_ping_loop_title), 552 mContext.getString(R.string.notification_ping_loop_text), pi); 553 nm.notify(Eas.EXCHANGE_ERROR_NOTIFICATION, note); 554 } 555 } 556 557 void runPingLoop() throws IOException, StaleFolderListException { 558 // Do push for all sync services here 559 ArrayList<Mailbox> pushBoxes = new ArrayList<Mailbox>(); 560 long endTime = System.currentTimeMillis() + (30*MINS); 561 HashMap<Long, Integer> pingFailureMap = new HashMap<Long, Integer>(); 562 563 while (System.currentTimeMillis() < endTime) { 564 // Count of pushable mailboxes 565 int pushCount = 0; 566 // Count of mailboxes that can be pushed right now 567 int canPushCount = 0; 568 Serializer s = new Serializer(); 569 int code; 570 Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, 571 MailboxColumns.ACCOUNT_KEY + '=' + mAccount.mId + 572 AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX, null, null); 573 574 pushBoxes.clear(); 575 576 try { 577 // Loop through our pushed boxes seeing what is available to push 578 while (c.moveToNext()) { 579 pushCount++; 580 // Two requirements for push: 581 // 1) SyncManager tells us the mailbox is syncable (not running, not stopped) 582 // 2) The syncKey isn't "0" (i.e. it's synced at least once) 583 long mailboxId = c.getLong(Mailbox.CONTENT_ID_COLUMN); 584 if (SyncManager.canSync(mailboxId)) { 585 String syncKey = c.getString(Mailbox.CONTENT_SYNC_KEY_COLUMN); 586 if (syncKey == null || syncKey.equals("0")) { 587 continue; 588 } 589 590 // Take a peek at this box's behavior last sync 591 // We do this because some Exchange 2003 servers put themselves (and 592 // therefore our client) into a "ping loop" in which the client is 593 // continuously told of server changes, only to find that there aren't any. 594 // This behavior is seemingly random, and we must code defensively by 595 // backing off of push behavior when this is detected. 596 // The server fix is at http://support.microsoft.com/kb/923282 597 598 // Sync status is encoded as S<type>:<exitstatus>:<changes> 599 String status = c.getString(Mailbox.CONTENT_SYNC_STATUS_COLUMN); 600 int type = SyncManager.getStatusType(status); 601 if (type == SyncManager.SYNC_PING) { 602 int changeCount = SyncManager.getStatusChangeCount(status); 603 if (changeCount == 0) { 604 // This means that a ping failed; we'll keep track of this 605 Integer failures = pingFailureMap.get(mailboxId); 606 if (failures == null) { 607 pingFailureMap.put(mailboxId, 1); 608 } else if (failures > 4) { 609 // Change all push/ping boxes (except account) to 5 minute sync 610 pushFallback(); 611 return; 612 } else { 613 pingFailureMap.put(mailboxId, failures + 1); 614 } 615 } else { 616 pingFailureMap.put(mailboxId, 0); 617 } 618 } 619 620 if (canPushCount++ == 0) { 621 // Initialize the Ping command 622 s.start(Tags.PING_PING).data(Tags.PING_HEARTBEAT_INTERVAL, "900") 623 .start(Tags.PING_FOLDERS); 624 } 625 // When we're ready for Calendar/Contacts, we will check folder type 626 // TODO Save Calendar and Contacts!! Mark as not visible! 627 String folderClass = getTargetCollectionClassFromCursor(c); 628 s.start(Tags.PING_FOLDER) 629 .data(Tags.PING_ID, c.getString(Mailbox.CONTENT_SERVER_ID_COLUMN)) 630 .data(Tags.PING_CLASS, folderClass) 631 .end(); 632 userLog("Ping ready for: " + folderClass + ", " + 633 c.getString(Mailbox.CONTENT_SERVER_ID_COLUMN) + " (" + 634 c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN) + ')'); 635 pushBoxes.add(new Mailbox().restore(c)); 636 } else { 637 userLog(c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN) + 638 " not ready for ping"); 639 } 640 } 641 } finally { 642 c.close(); 643 } 644 645 if (canPushCount > 0 && (canPushCount == pushCount)) { 646 // If we have some number that are ready for push, send Ping to the server 647 s.end().end().done(); 648 649 HttpResponse res = sendHttpClientPost(PING_COMMAND, s.toByteArray()); 650 Thread.currentThread().setName(mAccount.mDisplayName + ": Ping"); 651 //userLog("Sending ping, timeout: " + uc.getReadTimeout() / 1000 + "s"); 652 653 // Don't send request if we've been asked to stop 654 if (mStop) return; 655 long time = System.currentTimeMillis(); 656 code = res.getStatusLine().getStatusCode(); 657 658 // Return immediately if we've been asked to stop 659 if (mStop) { 660 userLog("Stopping pingLoop"); 661 return; 662 } 663 664 // Get elapsed time 665 time = System.currentTimeMillis() - time; 666 userLog("Ping response: " + code + " in " + time + "ms"); 667 668 if (code == HttpURLConnection.HTTP_OK) { 669 HttpEntity e = res.getEntity(); 670 int len = (int)e.getContentLength(); 671 InputStream is = res.getEntity().getContent(); 672 if (len > 0) { 673 parsePingResult(is, mContentResolver); 674 } else { 675 throw new IOException(); 676 } 677 } else if (isAuthError(code)) { 678 mExitStatus = AbstractSyncService.EXIT_LOGIN_FAILURE; 679 userLog("Authorization error during Ping: " + code); 680 throw new IOException(); 681 } 682 } else if (pushCount > 0) { 683 // If we want to Ping, but can't just yet, wait 10 seconds and try again 684 userLog("pingLoop waiting for " + (pushCount - canPushCount) + " box(es)"); 685 sleep(10*SECS); 686 } else { 687 // We've got nothing to do, so let's hang out for a while 688 sleep(20*MINS); 689 } 690 } 691 } 692 693 void sleep(long ms) { 694 try { 695 Thread.sleep(ms); 696 } catch (InterruptedException e) { 697 // Doesn't matter whether we stop early; it's the thought that counts 698 } 699 } 700 701 private int parsePingResult(InputStream is, ContentResolver cr) 702 throws IOException, StaleFolderListException { 703 PingParser pp = new PingParser(is, this); 704 if (pp.parse()) { 705 // True indicates some mailboxes need syncing... 706 // syncList has the serverId's of the mailboxes... 707 mBindArguments[0] = Long.toString(mAccount.mId); 708 ArrayList<String> syncList = pp.getSyncList(); 709 for (String serverId: syncList) { 710 mBindArguments[1] = serverId; 711 Cursor c = cr.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, 712 WHERE_ACCOUNT_KEY_AND_SERVER_ID, mBindArguments, null); 713 try { 714 if (c.moveToFirst()) { 715 SyncManager.startManualSync(c.getLong(Mailbox.CONTENT_ID_COLUMN), 716 SyncManager.SYNC_PING, null); 717 } 718 } finally { 719 c.close(); 720 } 721 } 722 } 723 return pp.getSyncList().size(); 724 } 725 726 ByteArrayInputStream readResponse(HttpURLConnection uc) throws IOException { 727 String encoding = uc.getHeaderField("Transfer-Encoding"); 728 if (encoding == null) { 729 int len = uc.getHeaderFieldInt("Content-Length", 0); 730 if (len > 0) { 731 InputStream in = uc.getInputStream(); 732 byte[] bytes = new byte[len]; 733 int remain = len; 734 int offs = 0; 735 while (remain > 0) { 736 int read = in.read(bytes, offs, remain); 737 remain -= read; 738 offs += read; 739 } 740 return new ByteArrayInputStream(bytes); 741 } 742 } else if (encoding.equalsIgnoreCase("chunked")) { 743 // TODO We don't handle this yet 744 return null; 745 } 746 return null; 747 } 748 749 String readResponseString(HttpURLConnection uc) throws IOException { 750 String encoding = uc.getHeaderField("Transfer-Encoding"); 751 if (encoding == null) { 752 int len = uc.getHeaderFieldInt("Content-Length", 0); 753 if (len > 0) { 754 InputStream in = uc.getInputStream(); 755 byte[] bytes = new byte[len]; 756 int remain = len; 757 int offs = 0; 758 while (remain > 0) { 759 int read = in.read(bytes, offs, remain); 760 remain -= read; 761 offs += read; 762 } 763 return new String(bytes); 764 } 765 } else if (encoding.equalsIgnoreCase("chunked")) { 766 // TODO We don't handle this yet 767 return null; 768 } 769 return null; 770 } 771 772 private String getFilterType() { 773 String filter = Eas.FILTER_1_WEEK; 774 switch (mAccount.mSyncLookback) { 775 case com.android.email.Account.SYNC_WINDOW_1_DAY: { 776 filter = Eas.FILTER_1_DAY; 777 break; 778 } 779 case com.android.email.Account.SYNC_WINDOW_3_DAYS: { 780 filter = Eas.FILTER_3_DAYS; 781 break; 782 } 783 case com.android.email.Account.SYNC_WINDOW_1_WEEK: { 784 filter = Eas.FILTER_1_WEEK; 785 break; 786 } 787 case com.android.email.Account.SYNC_WINDOW_2_WEEKS: { 788 filter = Eas.FILTER_2_WEEKS; 789 break; 790 } 791 case com.android.email.Account.SYNC_WINDOW_1_MONTH: { 792 filter = Eas.FILTER_1_MONTH; 793 break; 794 } 795 case com.android.email.Account.SYNC_WINDOW_ALL: { 796 filter = Eas.FILTER_ALL; 797 break; 798 } 799 } 800 return filter; 801 } 802 803 /** 804 * Common code to sync E+PIM data 805 * 806 * @param target, an EasMailbox, EasContacts, or EasCalendar object 807 */ 808 public void sync(AbstractSyncAdapter target) throws IOException { 809 mTarget = target; 810 Mailbox mailbox = target.mMailbox; 811 812 boolean moreAvailable = true; 813 while (!mStop && moreAvailable) { 814 runAwake(); 815 waitForConnectivity(); 816 817 while (true) { 818 PartRequest req = null; 819 synchronized (mPartRequests) { 820 if (mPartRequests.isEmpty()) { 821 break; 822 } else { 823 req = mPartRequests.get(0); 824 } 825 } 826 getAttachment(req); 827 synchronized(mPartRequests) { 828 mPartRequests.remove(req); 829 } 830 } 831 832 Serializer s = new Serializer(); 833 if (mailbox.mSyncKey == null) { 834 userLog("Mailbox syncKey RESET"); 835 mailbox.mSyncKey = "0"; 836 } 837 String className = target.getCollectionName(); 838 userLog("Sending " + className + " syncKey: " + mailbox.mSyncKey); 839 s.start(Tags.SYNC_SYNC) 840 .start(Tags.SYNC_COLLECTIONS) 841 .start(Tags.SYNC_COLLECTION) 842 .data(Tags.SYNC_CLASS, className) 843 .data(Tags.SYNC_SYNC_KEY, mailbox.mSyncKey) 844 .data(Tags.SYNC_COLLECTION_ID, mailbox.mServerId) 845 .tag(Tags.SYNC_DELETES_AS_MOVES); 846 847 // EAS doesn't like GetChanges if the syncKey is "0"; not documented 848 if (!mailbox.mSyncKey.equals("0")) { 849 s.tag(Tags.SYNC_GET_CHANGES); 850 } 851 s.data(Tags.SYNC_WINDOW_SIZE, 852 className.equals("Email") ? EMAIL_WINDOW_SIZE : PIM_WINDOW_SIZE); 853 boolean options = false; 854 if (!className.equals("Contacts")) { 855 // Set the lookback appropriately (EAS calls this a "filter") 856 s.start(Tags.SYNC_OPTIONS).data(Tags.SYNC_FILTER_TYPE, getFilterType()); 857 // No truncation in this version 858 //if (mProtocolVersionDouble < 12.0) { 859 // s.data(Tags.SYNC_TRUNCATION, "7"); 860 //} 861 options = true; 862 } 863 if (mProtocolVersionDouble >= 12.0) { 864 if (!options) { 865 options = true; 866 s.start(Tags.SYNC_OPTIONS); 867 } 868 s.start(Tags.BASE_BODY_PREFERENCE) 869 // HTML for email; plain text for everything else 870 .data(Tags.BASE_TYPE, (className.equals("Email") ? Eas.BODY_PREFERENCE_HTML 871 : Eas.BODY_PREFERENCE_TEXT)) 872 // No truncation in this version 873 //.data(Tags.BASE_TRUNCATION_SIZE, Eas.DEFAULT_BODY_TRUNCATION_SIZE) 874 .end(); 875 } 876 if (options) { 877 s.end(); 878 } 879 880 // Send our changes up to the server 881 target.sendLocalChanges(s, this); 882 883 s.end().end().end().done(); 884 HttpResponse resp = sendHttpClientPost("Sync", s.toByteArray()); 885 int code = resp.getStatusLine().getStatusCode(); 886 if (code == HttpURLConnection.HTTP_OK) { 887 InputStream is = resp.getEntity().getContent(); 888 if (is != null) { 889 moreAvailable = target.parse(is, this); 890 target.cleanup(this); 891 } 892 } else { 893 userLog("Sync response error: " + code); 894 if (isAuthError(code)) { 895 mExitStatus = AbstractSyncService.EXIT_LOGIN_FAILURE; 896 } 897 return; 898 } 899 } 900 } 901 902 /* (non-Javadoc) 903 * @see java.lang.Runnable#run() 904 */ 905 public void run() { 906 mThread = Thread.currentThread(); 907 TAG = mThread.getName(); 908 909 HostAuth ha = HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv); 910 mHostAddress = ha.mAddress; 911 mUserName = ha.mLogin; 912 mPassword = ha.mPassword; 913 914 try { 915 SyncManager.callback().syncMailboxStatus(mMailboxId, EmailServiceStatus.IN_PROGRESS, 0); 916 } catch (RemoteException e1) { 917 // Don't care if this fails 918 } 919 920 // Make sure account and mailbox are always the latest from the database 921 mAccount = Account.restoreAccountWithId(mContext, mAccount.mId); 922 mMailbox = Mailbox.restoreMailboxWithId(mContext, mMailbox.mId); 923 // Whether or not we're the account mailbox 924 boolean accountMailbox = false; 925 try { 926 mDeviceId = SyncManager.getDeviceId(); 927 if (mMailbox == null || mAccount == null) { 928 return; 929 } else if (mMailbox.mType == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) { 930 accountMailbox = true; 931 runAccountMailbox(); 932 } else { 933 AbstractSyncAdapter target; 934 mAccount = Account.restoreAccountWithId(mContext, mAccount.mId); 935 mProtocolVersion = mAccount.mProtocolVersion; 936 mProtocolVersionDouble = Double.parseDouble(mProtocolVersion); 937 if (mMailbox.mType == Mailbox.TYPE_CONTACTS) 938 target = new ContactsSyncAdapter(mMailbox, this); 939 else { 940 target = new EmailSyncAdapter(mMailbox, this); 941 } 942 // We loop here because someone might have put a request in while we were syncing 943 // and we've missed that opportunity... 944 do { 945 if (mRequestTime != 0) { 946 userLog("Looping for user request..."); 947 mRequestTime = 0; 948 } 949 sync(target); 950 } while (mRequestTime != 0); 951 } 952 mExitStatus = EXIT_DONE; 953 } catch (IOException e) { 954 userLog("Caught IOException"); 955 mExitStatus = EXIT_IO_ERROR; 956 } catch (Exception e) { 957 e.printStackTrace(); 958 } finally { 959 if (!mStop) { 960 userLog(mMailbox.mDisplayName + ": sync finished"); 961 SyncManager.done(this); 962 // If this is the account mailbox, wake up SyncManager 963 // Because this box has a "push" interval, it will be restarted immediately 964 // which will cause the folder list to be reloaded... 965 try { 966 int status; 967 switch (mExitStatus) { 968 case EXIT_IO_ERROR: 969 status = EmailServiceStatus.CONNECTION_ERROR; 970 break; 971 case EXIT_DONE: 972 status = EmailServiceStatus.SUCCESS; 973 break; 974 case EXIT_LOGIN_FAILURE: 975 status = EmailServiceStatus.LOGIN_FAILED; 976 break; 977 default: 978 status = EmailServiceStatus.REMOTE_EXCEPTION; 979 break; 980 } 981 SyncManager.callback().syncMailboxStatus(mMailboxId, status, 0); 982 983 // Save the sync time and status 984 ContentValues cv = new ContentValues(); 985 cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis()); 986 String s = "S" + mSyncReason + ':' + status + ':' + mChangeCount; 987 cv.put(Mailbox.SYNC_STATUS, s); 988 mContentResolver.update(ContentUris 989 .withAppendedId(Mailbox.CONTENT_URI, mMailboxId), cv, null, null); 990 } catch (RemoteException e1) { 991 // Don't care if this fails 992 } 993 } else { 994 userLog(mMailbox.mDisplayName + ": stopped thread finished."); 995 } 996 997 // Make sure this gets restarted... 998 if (accountMailbox) { 999 SyncManager.kick("account mailbox stopped"); 1000 } 1001 } 1002 } 1003} 1004