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