EasSyncService.java revision 618966d6899b6a517bdd77b20eb9dd62cea4f6f5
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.mail.AuthenticationFailedException; 21import com.android.email.mail.MessagingException; 22import com.android.email.provider.EmailContent.Account; 23import com.android.email.provider.EmailContent.AccountColumns; 24import com.android.email.provider.EmailContent.Attachment; 25import com.android.email.provider.EmailContent.AttachmentColumns; 26import com.android.email.provider.EmailContent.HostAuth; 27import com.android.email.provider.EmailContent.Mailbox; 28import com.android.email.provider.EmailContent.MailboxColumns; 29import com.android.email.provider.EmailContent.Message; 30import com.android.exchange.adapter.AbstractSyncAdapter; 31import com.android.exchange.adapter.ContactsSyncAdapter; 32import com.android.exchange.adapter.EmailSyncAdapter; 33import com.android.exchange.adapter.FolderSyncParser; 34import com.android.exchange.adapter.ItemEstimateParser; 35import com.android.exchange.adapter.PingParser; 36import com.android.exchange.adapter.Serializer; 37import com.android.exchange.adapter.Tags; 38import com.android.exchange.adapter.Parser.EasParserException; 39import com.android.exchange.utility.Base64; 40 41import org.apache.http.HttpEntity; 42import org.apache.http.HttpResponse; 43import org.apache.http.client.methods.HttpPost; 44import org.apache.http.conn.ssl.AllowAllHostnameVerifier; 45import org.apache.http.impl.client.DefaultHttpClient; 46 47import android.content.ContentResolver; 48import android.content.ContentValues; 49import android.content.Context; 50import android.database.Cursor; 51import android.os.RemoteException; 52 53import java.io.BufferedReader; 54import java.io.BufferedWriter; 55import java.io.ByteArrayInputStream; 56import java.io.File; 57import java.io.FileNotFoundException; 58import java.io.FileOutputStream; 59import java.io.FileReader; 60import java.io.FileWriter; 61import java.io.IOException; 62import java.io.InputStream; 63import java.io.OutputStreamWriter; 64import java.net.HttpURLConnection; 65import java.net.MalformedURLException; 66import java.net.ProtocolException; 67import java.net.URI; 68import java.net.URL; 69import java.net.URLEncoder; 70import java.util.ArrayList; 71 72import javax.net.ssl.HttpsURLConnection; 73 74public class EasSyncService extends InteractiveSyncService { 75 76 private static final String WINDOW_SIZE = "10"; 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_SYNC_INTERVAL_PING = 80 Mailbox.SYNC_INTERVAL + '=' + Account.CHECK_INTERVAL_PING; 81 private static final String AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX = " AND " + 82 MailboxColumns.SYNC_INTERVAL + " IN (" + Account.CHECK_INTERVAL_PING + 83 ',' + Account.CHECK_INTERVAL_PUSH + ") AND " + MailboxColumns.TYPE + "!=\"" + 84 Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + '\"'; 85 86 static private final int CHUNK_SIZE = 16 * 1024; 87 88 // Reasonable default 89 String mProtocolVersion = "2.5"; 90 public Double mProtocolVersionDouble; 91 static String mDeviceId = null; 92 static String mDeviceType = "Android"; 93 AbstractSyncAdapter mTarget; 94 String mAuthString = null; 95 String mCmdString = null; 96 String mVersions; 97 public String mHostAddress; 98 public String mUserName; 99 public String mPassword; 100 String mDomain = null; 101 boolean mSentCommands; 102 boolean mIsIdle = false; 103 boolean mSsl = true; 104 public Context mContext; 105 public ContentResolver mContentResolver; 106 String[] mBindArguments = new String[2]; 107 InputStream mPendingPartInputStream = null; 108 private volatile boolean mStop = false; 109 private Object mWaitTarget = new Object(); 110 111 public EasSyncService(Context _context, Mailbox _mailbox) { 112 super(_context, _mailbox); 113 mContext = _context; 114 mContentResolver = _context.getContentResolver(); 115 HostAuth ha = HostAuth.restoreHostAuthWithId(_context, mAccount.mHostAuthKeyRecv); 116 mSsl = (ha.mFlags & HostAuth.FLAG_SSL) != 0; 117 } 118 119 private EasSyncService(String prefix) { 120 super(prefix); 121 } 122 123 public EasSyncService() { 124 this("EAS Validation"); 125 } 126 127 @Override 128 public void ping() { 129 userLog("We've been pinged!"); 130 synchronized (mWaitTarget) { 131 mWaitTarget.notify(); 132 } 133 } 134 135 @Override 136 public void stop() { 137 mStop = true; 138 } 139 140 @Override 141 public int getSyncStatus() { 142 return 0; 143 } 144 145 private boolean isAuthError(int code) { 146 return (code == HttpURLConnection.HTTP_UNAUTHORIZED || code == HttpURLConnection.HTTP_FORBIDDEN 147 || code == HttpURLConnection.HTTP_INTERNAL_ERROR); 148 } 149 150 /* (non-Javadoc) 151 * @see com.android.exchange.SyncService#validateAccount(java.lang.String, java.lang.String, java.lang.String, int, boolean, android.content.Context) 152 */ 153 @Override 154 public void validateAccount(String hostAddress, String userName, String password, int port, 155 boolean ssl, Context context) throws MessagingException { 156 try { 157 userLog("Testing EAS: " + hostAddress + ", " + userName + ", ssl = " + ssl); 158 Serializer s = new Serializer(); 159 s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY).text("0") 160 .end().end().done(); 161 EasSyncService svc = new EasSyncService("%TestAccount%"); 162 svc.mHostAddress = hostAddress; 163 svc.mUserName = userName; 164 svc.mPassword = password; 165 svc.mSsl = ssl; 166 HttpURLConnection uc = svc.sendEASPostCommand("FolderSync", s.toString()); 167 int code = uc.getResponseCode(); 168 userLog("Validation response code: " + code); 169 if (code == HttpURLConnection.HTTP_OK) { 170 // No exception means successful validation 171 userLog("Validation successful"); 172 return; 173 } 174 if (isAuthError(code)) { 175 userLog("Authentication failed"); 176 throw new AuthenticationFailedException("Validation failed"); 177 } else { 178 // TODO Need to catch other kinds of errors (e.g. policy) For now, report the code. 179 userLog("Validation failed, reporting I/O error: " + code); 180 throw new MessagingException(MessagingException.IOERROR); 181 } 182 } catch (IOException e) { 183 userLog("IOException caught, reporting I/O error: " + e.getMessage()); 184 throw new MessagingException(MessagingException.IOERROR); 185 } 186 187 } 188 189 190 @Override 191 public void loadAttachment(Attachment att, IEmailServiceCallback cb) { 192 // TODO Auto-generated method stub 193 } 194 195 @Override 196 public void reloadFolderList() { 197 // TODO Auto-generated method stub 198 } 199 200 @Override 201 public void startSync() { 202 // TODO Auto-generated method stub 203 } 204 205 @Override 206 public void stopSync() { 207 // TODO Auto-generated method stub 208 } 209 210 protected HttpURLConnection sendEASPostCommand(String cmd, String data) throws IOException { 211 HttpURLConnection uc = setupEASCommand("POST", cmd); 212 if (uc != null) { 213 uc.setRequestProperty("Content-Length", Integer.toString(data.length() + 2)); 214 OutputStreamWriter w = new OutputStreamWriter(uc.getOutputStream(), "UTF-8"); 215 w.write(data); 216 w.write("\r\n"); 217 w.flush(); 218 w.close(); 219 } 220 return uc; 221 } 222 223 private void doStatusCallback(long messageId, long attachmentId, int status) { 224 try { 225 SyncManager.callback().loadAttachmentStatus(messageId, attachmentId, status, 0); 226 } catch (RemoteException e) { 227 // No danger if the client is no longer around 228 } 229 } 230 231 private void doProgressCallback(long messageId, long attachmentId, int progress) { 232 try { 233 SyncManager.callback().loadAttachmentStatus(messageId, attachmentId, 234 EmailServiceStatus.IN_PROGRESS, progress); 235 } catch (RemoteException e) { 236 // No danger if the client is no longer around 237 } 238 } 239 240 public File createUniqueFileInternal(String dir, String filename) { 241 File directory; 242 if (dir == null) { 243 directory = mContext.getFilesDir(); 244 } else { 245 directory = new File(dir); 246 } 247 if (!directory.exists()) { 248 directory.mkdirs(); 249 } 250 File file = new File(directory, filename); 251 if (!file.exists()) { 252 return file; 253 } 254 // Get the extension of the file, if any. 255 int index = filename.lastIndexOf('.'); 256 String name = filename; 257 String extension = ""; 258 if (index != -1) { 259 name = filename.substring(0, index); 260 extension = filename.substring(index); 261 } 262 for (int i = 2; i < Integer.MAX_VALUE; i++) { 263 file = new File(directory, name + '-' + i + extension); 264 if (!file.exists()) { 265 return file; 266 } 267 } 268 return null; 269 } 270 271 /** 272 * Loads an attachment, based on the PartRequest passed in. The PartRequest is basically our 273 * wrapper for Attachment 274 * @param req the part (attachment) to be retrieved 275 * @throws IOException 276 */ 277 protected void getAttachment(PartRequest req) throws IOException { 278 Attachment att = req.att; 279 Message msg = Message.restoreMessageWithId(mContext, att.mMessageKey); 280 doProgressCallback(msg.mId, att.mId, 0); 281 DefaultHttpClient client = new DefaultHttpClient(); 282 String us = makeUriString("GetAttachment", "&AttachmentName=" + att.mLocation); 283 HttpPost method = new HttpPost(URI.create(us)); 284 method.setHeader("Authorization", mAuthString); 285 286 HttpResponse res = client.execute(method); 287 int status = res.getStatusLine().getStatusCode(); 288 if (status == HttpURLConnection.HTTP_OK) { 289 HttpEntity e = res.getEntity(); 290 int len = (int)e.getContentLength(); 291 String type = e.getContentType().getValue(); 292 InputStream is = res.getEntity().getContent(); 293 File f = (req.destination != null) 294 ? new File(req.destination) 295 : createUniqueFileInternal(req.destination, att.mFileName); 296 if (f != null) { 297 // Ensure that the target directory exists 298 File destDir = f.getParentFile(); 299 if (!destDir.exists()) { 300 destDir.mkdirs(); 301 } 302 FileOutputStream os = new FileOutputStream(f); 303 if (len > 0) { 304 try { 305 mPendingPartRequest = req; 306 mPendingPartInputStream = is; 307 byte[] bytes = new byte[CHUNK_SIZE]; 308 int length = len; 309 while (len > 0) { 310 int n = (len > CHUNK_SIZE ? CHUNK_SIZE : len); 311 int read = is.read(bytes, 0, n); 312 os.write(bytes, 0, read); 313 len -= read; 314 int pct = ((length - len) * 100 / length); 315 doProgressCallback(msg.mId, att.mId, pct); 316 } 317 } finally { 318 mPendingPartRequest = null; 319 mPendingPartInputStream = null; 320 } 321 } 322 os.flush(); 323 os.close(); 324 325 // EmailProvider will throw an exception if we try to update an unsaved attachment 326 if (att.isSaved()) { 327 String contentUriString = (req.contentUriString != null) 328 ? req.contentUriString 329 : "file://" + f.getAbsolutePath(); 330 ContentValues cv = new ContentValues(); 331 cv.put(AttachmentColumns.CONTENT_URI, contentUriString); 332 cv.put(AttachmentColumns.MIME_TYPE, type); 333 att.update(mContext, cv); 334 doStatusCallback(msg.mId, att.mId, EmailServiceStatus.SUCCESS); 335 } 336 } 337 } else { 338 doStatusCallback(msg.mId, att.mId, EmailServiceStatus.MESSAGE_NOT_FOUND); 339 } 340 } 341 342 private HttpURLConnection setupEASCommand(String method, String cmd) throws IOException { 343 return setupEASCommand(method, cmd, null); 344 } 345 346 @SuppressWarnings("deprecation") 347 private String makeUriString(String cmd, String extra) { 348 // Cache the authentication string and the command string 349 if (mDeviceId == null) 350 mDeviceId = "droidfu"; 351 String safeUserName = URLEncoder.encode(mUserName); 352 if (mAuthString == null) { 353 String cs = mUserName + ':' + mPassword; 354 mAuthString = "Basic " + Base64.encodeBytes(cs.getBytes()); 355 mCmdString = "&User=" + safeUserName + "&DeviceId=" + mDeviceId + "&DeviceType=" 356 + mDeviceType; 357 } 358 359 String us = (mSsl ? "https" : "http") + "://" + mHostAddress + 360 "/Microsoft-Server-ActiveSync"; 361 if (cmd != null) { 362 us += "?Cmd=" + cmd + mCmdString; 363 } 364 if (extra != null) { 365 us += extra; 366 } 367 return us; 368 } 369 370 private HttpURLConnection setupEASCommand(String method, String cmd, String extra) 371 throws IOException { 372 try { 373 String us = makeUriString(cmd, extra); 374 URL u = new URL(us); 375 HttpURLConnection uc = (HttpURLConnection)u.openConnection(); 376 HttpURLConnection.setFollowRedirects(true); 377 378 if (mSsl) { 379 ((HttpsURLConnection)uc).setHostnameVerifier(new AllowAllHostnameVerifier()); 380 } 381 382 uc.setConnectTimeout(10 * SECS); 383 uc.setReadTimeout(20 * MINS); 384 if (method.equals("POST")) { 385 uc.setDoOutput(true); 386 } 387 uc.setRequestMethod(method); 388 uc.setRequestProperty("Authorization", mAuthString); 389 390 if (extra == null) { 391 if (cmd != null && cmd.startsWith("SendMail&")) { 392 uc.setRequestProperty("Content-Type", "message/rfc822"); 393 } else { 394 uc.setRequestProperty("Content-Type", "application/vnd.ms-sync.wbxml"); 395 } 396 uc.setRequestProperty("MS-ASProtocolVersion", mProtocolVersion); 397 uc.setRequestProperty("Connection", "keep-alive"); 398 uc.setRequestProperty("User-Agent", mDeviceType + '/' + Eas.VERSION); 399 } else { 400 uc.setRequestProperty("Content-Length", "0"); 401 } 402 403 return uc; 404 } catch (MalformedURLException e) { 405 // TODO See if there is a better exception to throw here and below 406 throw new IOException(); 407 } catch (ProtocolException e) { 408 throw new IOException(); 409 } 410 } 411 412 String getTargetCollectionClassFromCursor(Cursor c) { 413 int type = c.getInt(Mailbox.CONTENT_TYPE_COLUMN); 414 if (type == Mailbox.TYPE_CONTACTS) { 415 return "Contacts"; 416 } else if (type == Mailbox.TYPE_CALENDAR) { 417 return "Calendar"; 418 } else { 419 return "Email"; 420 } 421 } 422 423 /** 424 * Performs FolderSync 425 * 426 * @throws IOException 427 * @throws EasParserException 428 */ 429 public void runAccountMailbox() throws IOException, EasParserException { 430 // Initialize exit status to success 431 mExitStatus = EmailServiceStatus.SUCCESS; 432 try { 433 try { 434 SyncManager.callback() 435 .syncMailboxListStatus(mAccount.mId, EmailServiceStatus.IN_PROGRESS, 0); 436 } catch (RemoteException e1) { 437 // Don't care if this fails 438 } 439 440 if (mAccount.mSyncKey == null) { 441 mAccount.mSyncKey = "0"; 442 userLog("Account syncKey RESET"); 443 ContentValues cv = new ContentValues(); 444 cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey); 445 mAccount.update(mContext, cv); 446 } 447 448 // When we first start up, change all ping mailboxes to push. 449 ContentValues cv = new ContentValues(); 450 cv.put(Mailbox.SYNC_INTERVAL, Account.CHECK_INTERVAL_PUSH); 451 if (mContentResolver.update(Mailbox.CONTENT_URI, cv, 452 WHERE_SYNC_INTERVAL_PING, null) > 0) { 453 SyncManager.kick(); 454 } 455 456 userLog("Account syncKey: " + mAccount.mSyncKey); 457 // Determine our protocol version, if we haven't already 458 if (mAccount.mProtocolVersion == null) { 459 HttpURLConnection uc = setupEASCommand("OPTIONS", null); 460 if (uc != null) { 461 int code = uc.getResponseCode(); 462 userLog("OPTIONS response: " + code); 463 if (code == HttpURLConnection.HTTP_OK) { 464 mVersions = uc.getHeaderField("ms-asprotocolversions"); 465 if (mVersions != null) { 466 if (mVersions.contains("12.0")) { 467 mProtocolVersion = "12.0"; 468 } 469 mProtocolVersionDouble = Double.parseDouble(mProtocolVersion); 470 mAccount.mProtocolVersion = mProtocolVersion; 471 userLog(mVersions); 472 userLog("Using version " + mProtocolVersion); 473 } else { 474 errorLog("No protocol versions in OPTIONS response"); 475 throw new IOException(); 476 } 477 } else { 478 errorLog("OPTIONS command failed; throwing IOException"); 479 throw new IOException(); 480 } 481 } 482 } 483 while (!mStop) { 484 Serializer s = new Serializer(); 485 s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY) 486 .text(mAccount.mSyncKey).end().end().done(); 487 HttpURLConnection uc = sendEASPostCommand("FolderSync", s.toString()); 488 int code = uc.getResponseCode(); 489 if (code == HttpURLConnection.HTTP_OK) { 490 String encoding = uc.getHeaderField("Transfer-Encoding"); 491 if (encoding == null) { 492 int len = uc.getHeaderFieldInt("Content-Length", 0); 493 if (len > 0) { 494 InputStream is = uc.getInputStream(); 495 // Returns true if we need to sync again 496 if (new FolderSyncParser(is, this).parse()) { 497 continue; 498 } 499 } 500 } else if (encoding.equalsIgnoreCase("chunked")) { 501 // TODO We don't handle this yet 502 } 503 } else if (code == HttpURLConnection.HTTP_UNAUTHORIZED || 504 code == HttpURLConnection.HTTP_FORBIDDEN) { 505 mExitStatus = AbstractSyncService.EXIT_LOGIN_FAILURE; 506 } else { 507 userLog("FolderSync response error: " + code); 508 } 509 510 try { 511 SyncManager.callback() 512 .syncMailboxListStatus(mAccount.mId, mExitStatus, 0); 513 } catch (RemoteException e1) { 514 // Don't care if this fails 515 } 516 517 // Wait for push notifications. 518 String threadName = Thread.currentThread().getName(); 519 try { 520 runPingLoop(); 521 } catch (StaleFolderListException e) { 522 // We break out if we get told about a stale folder list 523 userLog("Ping interrupted; folder list requires sync..."); 524 } finally { 525 Thread.currentThread().setName(threadName); 526 } 527 } 528 } catch (IOException e) { 529 // We catch this here to send the folder sync status callback 530 // A folder sync failed callback will get sent from run() 531 try { 532 if (!mStop) { 533 SyncManager.callback() 534 .syncMailboxListStatus(mAccount.mId, 535 EmailServiceStatus.CONNECTION_ERROR, 0); 536 } 537 } catch (RemoteException e1) { 538 // Don't care if this fails 539 } 540 throw new IOException(); 541 } 542 } 543 544 private void getItemEstimate(Mailbox mailbox) throws IOException { 545 Serializer s = new Serializer(); 546 s.start(Tags.GIE_GET_ITEM_ESTIMATE).start(Tags.GIE_COLLECTIONS).start(Tags.GIE_COLLECTION); 547 String className = "Email"; 548 if (mailbox.mType == Mailbox.TYPE_CONTACTS) { 549 className = "Contacts"; 550 } 551 s.data(Tags.GIE_CLASS, className); 552 if (mailbox.mType < Mailbox.TYPE_NOT_EMAIL) { 553 s.data(Tags.GIE_COLLECTION_ID, mailbox.mServerId); 554 } 555 if (mailbox.mType != Mailbox.TYPE_CONTACTS) { 556 s.data(Tags.SYNC_FILTER_TYPE, getFilterType()); 557 } 558 s.data(Tags.SYNC_SYNC_KEY, mailbox.mSyncKey).end().end().end().done(); 559 HttpURLConnection uc = sendEASPostCommand("GetItemEstimate", s.toString()); 560 int code = uc.getResponseCode(); 561 if (code == HttpURLConnection.HTTP_OK) { 562 String encoding = uc.getHeaderField("Transfer-Encoding"); 563 if (encoding == null) { 564 int len = uc.getHeaderFieldInt("Content-Length", 0); 565 if (len > 0) { 566 InputStream is = uc.getInputStream(); 567 // Returns true if we need to sync again 568 if (new ItemEstimateParser(is, this).parse()) { 569 } 570 } 571 } else if (encoding.equalsIgnoreCase("chunked")) { 572 // TODO We don't handle this yet 573 } 574 } 575 } 576 577 void runPingLoop() throws IOException, StaleFolderListException { 578 // Do push for all sync services here 579 ArrayList<Mailbox> pushBoxes = new ArrayList<Mailbox>(); 580 long endTime = System.currentTimeMillis() + (30*MINS); 581 582 while (System.currentTimeMillis() < endTime) { 583 // Count of pushable mailboxes 584 int pushCount = 0; 585 // Count of mailboxes that can be pushed right now 586 int canPushCount = 0; 587 Serializer s = new Serializer(); 588 HttpURLConnection uc; 589 int code; 590 Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, 591 MailboxColumns.ACCOUNT_KEY + '=' + mAccount.mId + 592 AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX, null, null); 593 594 pushBoxes.clear(); 595 596 try { 597 // Loop through our pushed boxes seeing what is available to push 598 while (c.moveToNext()) { 599 pushCount++; 600 // Two requirements for push: 601 // 1) SyncManager tells us the mailbox is syncable (not running, not stopped) 602 // 2) The syncKey isn't "0" (i.e. it's synced at least once) 603 if (SyncManager.canSync(c.getLong(Mailbox.CONTENT_ID_COLUMN))) { 604 String syncKey = c.getString(Mailbox.CONTENT_SYNC_KEY_COLUMN); 605 if (syncKey == null || syncKey.equals("0")) { 606 continue; 607 } 608 if (canPushCount++ == 0) { 609 // Initialize the Ping command 610 s.start(Tags.PING_PING).data(Tags.PING_HEARTBEAT_INTERVAL, "900") 611 .start(Tags.PING_FOLDERS); 612 } 613 // When we're ready for Calendar/Contacts, we will check folder type 614 // TODO Save Calendar and Contacts!! Mark as not visible! 615 String folderClass = getTargetCollectionClassFromCursor(c); 616 s.start(Tags.PING_FOLDER) 617 .data(Tags.PING_ID, c.getString(Mailbox.CONTENT_SERVER_ID_COLUMN)) 618 .data(Tags.PING_CLASS, folderClass) 619 .end(); 620 userLog("Ping ready for: " + folderClass + ", " + 621 c.getString(Mailbox.CONTENT_SERVER_ID_COLUMN) + " (" + 622 c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN) + ')'); 623 pushBoxes.add(new Mailbox().restore(c)); 624 } else { 625 userLog(c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN) + 626 " not ready for ping"); 627 } 628 } 629 } finally { 630 c.close(); 631 } 632 633 if (canPushCount > 0 && (canPushCount == pushCount)) { 634 // If we have some number that are ready for push, send Ping to the server 635 s.end().end().done(); 636 637 uc = sendEASPostCommand("Ping", s.toString()); 638 Thread.currentThread().setName(mAccount.mDisplayName + ": Ping"); 639 userLog("Sending ping, timeout: " + uc.getReadTimeout() / 1000 + "s"); 640 // Don't send request if we've been asked to stop 641 if (mStop) return; 642 long time = System.currentTimeMillis(); 643 code = uc.getResponseCode(); 644 645 // Return immediately if we've been asked to stop 646 if (mStop) { 647 userLog("Stopping pingLoop"); 648 return; 649 } 650 651 // Get elapsed time 652 time = System.currentTimeMillis() - time; 653 userLog("Ping response: " + code + " in " + time + "ms"); 654 655 if (time < 2*SECS) { 656 // This is the Ping loop situation; try sending an item estimate 657 userLog("Trying getItemEstimate to break ping loop"); 658 for (Mailbox m: pushBoxes) { 659 getItemEstimate(m); 660 } 661 } 662 663 if (code == HttpURLConnection.HTTP_OK) { 664 String encoding = uc.getHeaderField("Transfer-Encoding"); 665 if (encoding == null) { 666 int len = uc.getHeaderFieldInt("Content-Length", 0); 667 if (len > 0) { 668 parsePingResult(uc, mContentResolver); 669 } else { 670 // This implies a connection issue that we can't handle 671 throw new IOException(); 672 } 673 } else { 674 // It shouldn't be possible for EAS server to send chunked data here 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(10*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(HttpURLConnection uc, ContentResolver cr) 702 throws IOException, StaleFolderListException { 703 PingParser pp = new PingParser(uc.getInputStream(), 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 null, "Ping"); 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 * EAS requires a unique device id, so that sync is possible from a variety of different 805 * devices (e.g. the syncKey is specific to a device) If we're on an emulator or some other 806 * device that doesn't provide one, we can create it as droid<n> where <n> is system time. 807 * This would work on a real device as well, but it would be better to use the "real" id if 808 * it's available 809 */ 810 private String getSimulatedDeviceId() { 811 try { 812 File f = mContext.getFileStreamPath("deviceName"); 813 BufferedReader rdr = null; 814 String id; 815 if (f.exists() && f.canRead()) { 816 rdr = new BufferedReader(new FileReader(f), 128); 817 id = rdr.readLine(); 818 rdr.close(); 819 return id; 820 } else if (f.createNewFile()) { 821 BufferedWriter w = new BufferedWriter(new FileWriter(f)); 822 id = "droid" + System.currentTimeMillis(); 823 w.write(id); 824 w.close(); 825 } 826 } catch (FileNotFoundException e) { 827 // We'll just use the default below 828 } catch (IOException e) { 829 // We'll just use the default below 830 } 831 return "droid0"; 832 } 833 834 /** 835 * Common code to sync E+PIM data 836 * 837 * @param target, an EasMailbox, EasContacts, or EasCalendar object 838 */ 839 public void sync(AbstractSyncAdapter target) throws IOException { 840 mTarget = target; 841 Mailbox mailbox = target.mMailbox; 842 843 boolean moreAvailable = true; 844 while (!mStop && moreAvailable) { 845 runAwake(); 846 waitForConnectivity(); 847 848 while (true) { 849 PartRequest req = null; 850 synchronized (mPartRequests) { 851 if (mPartRequests.isEmpty()) { 852 break; 853 } else { 854 req = mPartRequests.get(0); 855 } 856 } 857 getAttachment(req); 858 synchronized(mPartRequests) { 859 mPartRequests.remove(req); 860 } 861 } 862 863 Serializer s = new Serializer(); 864 if (mailbox.mSyncKey == null) { 865 userLog("Mailbox syncKey RESET"); 866 mailbox.mSyncKey = "0"; 867 mailbox.mSyncInterval = Account.CHECK_INTERVAL_PUSH; 868 } 869 String className = target.getCollectionName(); 870 userLog("Sending " + className + " syncKey: " + mailbox.mSyncKey); 871 s.start(Tags.SYNC_SYNC) 872 .start(Tags.SYNC_COLLECTIONS) 873 .start(Tags.SYNC_COLLECTION) 874 .data(Tags.SYNC_CLASS, className) 875 .data(Tags.SYNC_SYNC_KEY, mailbox.mSyncKey) 876 .data(Tags.SYNC_COLLECTION_ID, mailbox.mServerId) 877 .tag(Tags.SYNC_DELETES_AS_MOVES); 878 879 // EAS doesn't like GetChanges if the syncKey is "0"; not documented 880 if (!mailbox.mSyncKey.equals("0")) { 881 s.tag(Tags.SYNC_GET_CHANGES); 882 } 883 s.data(Tags.SYNC_WINDOW_SIZE, WINDOW_SIZE); 884 boolean options = false; 885 if (!className.equals("Contacts")) { 886 // Set the lookback appropriately (EAS calls this a "filter") 887 s.start(Tags.SYNC_OPTIONS).data(Tags.SYNC_FILTER_TYPE, getFilterType()); 888 // No truncation in this version 889 //if (mProtocolVersionDouble < 12.0) { 890 // s.data(Tags.SYNC_TRUNCATION, "7"); 891 //} 892 options = true; 893 } 894 if (mProtocolVersionDouble >= 12.0) { 895 if (!options) { 896 options = true; 897 s.start(Tags.SYNC_OPTIONS); 898 } 899 s.start(Tags.BASE_BODY_PREFERENCE) 900 // HTML for email; plain text for everything else 901 .data(Tags.BASE_TYPE, (className.equals("Email") ? Eas.BODY_PREFERENCE_HTML 902 : Eas.BODY_PREFERENCE_TEXT)) 903 // No truncation in this version 904 //.data(Tags.BASE_TRUNCATION_SIZE, Eas.DEFAULT_BODY_TRUNCATION_SIZE) 905 .end(); 906 } 907 if (options) { 908 s.end(); 909 } 910 911 // Send our changes up to the server 912 target.sendLocalChanges(s, this); 913 914 s.end().end().end().done(); 915 HttpURLConnection uc = sendEASPostCommand("Sync", s.toString()); 916 int code = uc.getResponseCode(); 917 if (code == HttpURLConnection.HTTP_OK) { 918 ByteArrayInputStream is = readResponse(uc); 919 if (is != null) { 920 moreAvailable = target.parse(is, this); 921 target.cleanup(this); 922 } 923 } else { 924 userLog("Sync response error: " + code); 925 if (isAuthError(code)) { 926 mExitStatus = AbstractSyncService.EXIT_LOGIN_FAILURE; 927 } 928 return; 929 } 930 } 931 } 932 933 /* (non-Javadoc) 934 * @see java.lang.Runnable#run() 935 */ 936 public void run() { 937 mThread = Thread.currentThread(); 938 TAG = mThread.getName(); 939 mDeviceId = android.provider.Settings.Secure.getString(mContext.getContentResolver(), 940 android.provider.Settings.Secure.ANDROID_ID); 941 // Generate a device id if we don't have one 942 if (mDeviceId == null) { 943 mDeviceId = getSimulatedDeviceId(); 944 } 945 HostAuth ha = HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv); 946 mHostAddress = ha.mAddress; 947 mUserName = ha.mLogin; 948 mPassword = ha.mPassword; 949 950 try { 951 SyncManager.callback().syncMailboxStatus(mMailboxId, EmailServiceStatus.IN_PROGRESS, 0); 952 } catch (RemoteException e1) { 953 // Don't care if this fails 954 } 955 956 // Make sure account and mailbox are always the latest from the database 957 mAccount = Account.restoreAccountWithId(mContext, mAccount.mId); 958 mMailbox = Mailbox.restoreMailboxWithId(mContext, mMailbox.mId); 959 // Whether or not we're the account mailbox 960 boolean accountMailbox = false; 961 try { 962 if (mMailbox == null || mAccount == null) { 963 return; 964 } else if (mMailbox.mType == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) { 965 accountMailbox = true; 966 runAccountMailbox(); 967 } else { 968 AbstractSyncAdapter target; 969 mAccount = Account.restoreAccountWithId(mContext, mAccount.mId); 970 mProtocolVersion = mAccount.mProtocolVersion; 971 mProtocolVersionDouble = Double.parseDouble(mProtocolVersion); 972 if (mMailbox.mType == Mailbox.TYPE_CONTACTS) 973 target = new ContactsSyncAdapter(mMailbox, this); 974 else { 975 target = new EmailSyncAdapter(mMailbox, this); 976 } 977 // We loop here because someone might have put a request in while we were syncing 978 // and we've missed that opportunity... 979 do { 980 if (mRequestTime != 0) { 981 userLog("Looping for user request..."); 982 mRequestTime = 0; 983 } 984 sync(target); 985 } while (mRequestTime != 0); 986 } 987 mExitStatus = EXIT_DONE; 988 } catch (IOException e) { 989 userLog("Caught IOException"); 990 mExitStatus = EXIT_IO_ERROR; 991 } catch (Exception e) { 992 e.printStackTrace(); 993 } finally { 994 if (!mStop) { 995 userLog(mMailbox.mDisplayName + ": sync finished"); 996 SyncManager.done(this); 997 // If this is the account mailbox, wake up SyncManager 998 // Because this box has a "push" interval, it will be restarted immediately 999 // which will cause the folder list to be reloaded... 1000 if (accountMailbox) { 1001 SyncManager.kick(); 1002 } 1003 try { 1004 int status; 1005 switch (mExitStatus) { 1006 case EXIT_IO_ERROR: 1007 status = EmailServiceStatus.CONNECTION_ERROR; 1008 break; 1009 case EXIT_DONE: 1010 status = EmailServiceStatus.SUCCESS; 1011 break; 1012 case EXIT_LOGIN_FAILURE: 1013 status = EmailServiceStatus.LOGIN_FAILED; 1014 break; 1015 default: 1016 status = EmailServiceStatus.REMOTE_EXCEPTION; 1017 break; 1018 } 1019 SyncManager.callback().syncMailboxStatus(mMailboxId, status, 0); 1020 } catch (RemoteException e1) { 1021 // Don't care if this fails 1022 } 1023 } else { 1024 userLog(mMailbox.mDisplayName + ": stopped thread finished."); 1025 } 1026 } 1027 } 1028} 1029