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