EasSyncService.java revision b1ffc81c7e3ee9297133a7924092192998ab3839
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.SecurityPolicy; 21import com.android.email.Utility; 22import com.android.email.SecurityPolicy.PolicySet; 23import com.android.email.mail.Address; 24import com.android.email.mail.AuthenticationFailedException; 25import com.android.email.mail.MeetingInfo; 26import com.android.email.mail.MessagingException; 27import com.android.email.mail.PackedString; 28import com.android.email.provider.EmailContent.Account; 29import com.android.email.provider.EmailContent.AccountColumns; 30import com.android.email.provider.EmailContent.Attachment; 31import com.android.email.provider.EmailContent.AttachmentColumns; 32import com.android.email.provider.EmailContent.HostAuth; 33import com.android.email.provider.EmailContent.Mailbox; 34import com.android.email.provider.EmailContent.MailboxColumns; 35import com.android.email.provider.EmailContent.Message; 36import com.android.email.service.EmailServiceProxy; 37import com.android.email.service.EmailServiceStatus; 38import com.android.exchange.adapter.AbstractSyncAdapter; 39import com.android.exchange.adapter.AccountSyncAdapter; 40import com.android.exchange.adapter.CalendarSyncAdapter; 41import com.android.exchange.adapter.ContactsSyncAdapter; 42import com.android.exchange.adapter.EmailSyncAdapter; 43import com.android.exchange.adapter.FolderSyncParser; 44import com.android.exchange.adapter.MeetingResponseParser; 45import com.android.exchange.adapter.PingParser; 46import com.android.exchange.adapter.ProvisionParser; 47import com.android.exchange.adapter.Serializer; 48import com.android.exchange.adapter.Tags; 49import com.android.exchange.adapter.Parser.EasParserException; 50import com.android.exchange.utility.CalendarUtilities; 51 52import org.apache.http.Header; 53import org.apache.http.HttpEntity; 54import org.apache.http.HttpResponse; 55import org.apache.http.HttpStatus; 56import org.apache.http.client.ClientProtocolException; 57import org.apache.http.client.HttpClient; 58import org.apache.http.client.methods.HttpOptions; 59import org.apache.http.client.methods.HttpPost; 60import org.apache.http.client.methods.HttpRequestBase; 61import org.apache.http.conn.ClientConnectionManager; 62import org.apache.http.entity.ByteArrayEntity; 63import org.apache.http.entity.StringEntity; 64import org.apache.http.impl.client.DefaultHttpClient; 65import org.apache.http.params.BasicHttpParams; 66import org.apache.http.params.HttpConnectionParams; 67import org.apache.http.params.HttpParams; 68import org.xmlpull.v1.XmlPullParser; 69import org.xmlpull.v1.XmlPullParserException; 70import org.xmlpull.v1.XmlPullParserFactory; 71import org.xmlpull.v1.XmlSerializer; 72 73import android.content.ContentResolver; 74import android.content.ContentUris; 75import android.content.ContentValues; 76import android.content.Context; 77import android.content.Entity; 78import android.database.Cursor; 79import android.os.Bundle; 80import android.os.RemoteException; 81import android.os.SystemClock; 82import android.provider.Calendar.Attendees; 83import android.provider.Calendar.Events; 84import android.util.Log; 85import android.util.Xml; 86import android.util.base64.Base64; 87 88import java.io.ByteArrayOutputStream; 89import java.io.File; 90import java.io.FileOutputStream; 91import java.io.IOException; 92import java.io.InputStream; 93import java.net.URI; 94import java.net.URLEncoder; 95import java.security.cert.CertificateException; 96import java.util.ArrayList; 97import java.util.HashMap; 98 99public class EasSyncService extends AbstractSyncService { 100 private static final String EMAIL_WINDOW_SIZE = "5"; 101 public static final String PIM_WINDOW_SIZE = "5"; 102 private static final String WHERE_ACCOUNT_KEY_AND_SERVER_ID = 103 MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SERVER_ID + "=?"; 104 private static final String WHERE_ACCOUNT_AND_SYNC_INTERVAL_PING = 105 MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SYNC_INTERVAL + 106 '=' + Mailbox.CHECK_INTERVAL_PING; 107 private static final String AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX = " AND " + 108 MailboxColumns.SYNC_INTERVAL + " IN (" + Mailbox.CHECK_INTERVAL_PING + 109 ',' + Mailbox.CHECK_INTERVAL_PUSH + ") AND " + MailboxColumns.TYPE + "!=\"" + 110 Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + '\"'; 111 private static final String WHERE_PUSH_HOLD_NOT_ACCOUNT_MAILBOX = 112 MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SYNC_INTERVAL + 113 '=' + Mailbox.CHECK_INTERVAL_PUSH_HOLD; 114 static private final int CHUNK_SIZE = 16*1024; 115 116 static private final String PING_COMMAND = "Ping"; 117 static private final int COMMAND_TIMEOUT = 20*SECONDS; 118 119 // Define our default protocol version as 2.5 (Exchange 2003) 120 static private final String DEFAULT_PROTOCOL_VERSION = "2.5"; 121 122 static private final String AUTO_DISCOVER_SCHEMA_PREFIX = 123 "http://schemas.microsoft.com/exchange/autodiscover/mobilesync/"; 124 static private final String AUTO_DISCOVER_PAGE = "/autodiscover/autodiscover.xml"; 125 static private final int AUTO_DISCOVER_REDIRECT_CODE = 451; 126 127 static public final String EAS_12_POLICY_TYPE = "MS-EAS-Provisioning-WBXML"; 128 static public final String EAS_2_POLICY_TYPE = "MS-WAP-Provisioning-XML"; 129 130 /** 131 * We start with an 8 minute timeout, and increase/decrease by 3 minutes at a time. There's 132 * no point having a timeout shorter than 5 minutes, I think; at that point, we can just let 133 * the ping exception out. The maximum I use is 17 minutes, which is really an empirical 134 * choice; too long and we risk silent connection loss and loss of push for that period. Too 135 * short and we lose efficiency/battery life. 136 * 137 * If we ever have to drop the ping timeout, we'll never increase it again. There's no point 138 * going into hysteresis; the NAT timeout isn't going to change without a change in connection, 139 * which will cause the sync service to be restarted at the starting heartbeat and going through 140 * the process again. 141 */ 142 static private final int PING_MINUTES = 60; // in seconds 143 static private final int PING_FUDGE_LOW = 10; 144 static private final int PING_STARTING_HEARTBEAT = (8*PING_MINUTES)-PING_FUDGE_LOW; 145 static private final int PING_MIN_HEARTBEAT = (5*PING_MINUTES)-PING_FUDGE_LOW; 146 static private final int PING_MAX_HEARTBEAT = (17*PING_MINUTES)-PING_FUDGE_LOW; 147 static private final int PING_HEARTBEAT_INCREMENT = 3*PING_MINUTES; 148 static private final int PING_FORCE_HEARTBEAT = 2*PING_MINUTES; 149 150 static private final int PROTOCOL_PING_STATUS_COMPLETED = 1; 151 152 // Fallbacks (in minutes) for ping loop failures 153 static private final int MAX_PING_FAILURES = 1; 154 static private final int PING_FALLBACK_INBOX = 5; 155 static private final int PING_FALLBACK_PIM = 25; 156 157 // MSFT's custom HTTP result code indicating the need to provision 158 static private final int HTTP_NEED_PROVISIONING = 449; 159 160 // Reasonable default 161 public String mProtocolVersion = DEFAULT_PROTOCOL_VERSION; 162 public Double mProtocolVersionDouble; 163 protected String mDeviceId = null; 164 private String mDeviceType = "Android"; 165 private String mAuthString = null; 166 private String mCmdString = null; 167 public String mHostAddress; 168 public String mUserName; 169 public String mPassword; 170 private boolean mSsl = true; 171 private boolean mTrustSsl = false; 172 public ContentResolver mContentResolver; 173 private String[] mBindArguments = new String[2]; 174 private ArrayList<String> mPingChangeList; 175 private HttpPost mPendingPost = null; 176 // The ping time (in seconds) 177 private int mPingHeartbeat = PING_STARTING_HEARTBEAT; 178 // The longest successful ping heartbeat 179 private int mPingHighWaterMark = 0; 180 // Whether we've ever lowered the heartbeat 181 private boolean mPingHeartbeatDropped = false; 182 // Whether a POST was aborted due to alarm (watchdog alarm) 183 private boolean mPostAborted = false; 184 // Whether a POST was aborted due to reset 185 private boolean mPostReset = false; 186 // Whether or not the sync service is valid (usable) 187 public boolean mIsValid = true; 188 189 public EasSyncService(Context _context, Mailbox _mailbox) { 190 super(_context, _mailbox); 191 mContentResolver = _context.getContentResolver(); 192 if (mAccount == null) { 193 mIsValid = false; 194 return; 195 } 196 HostAuth ha = HostAuth.restoreHostAuthWithId(_context, mAccount.mHostAuthKeyRecv); 197 if (ha == null) { 198 mIsValid = false; 199 return; 200 } 201 mSsl = (ha.mFlags & HostAuth.FLAG_SSL) != 0; 202 mTrustSsl = (ha.mFlags & HostAuth.FLAG_TRUST_ALL_CERTIFICATES) != 0; 203 } 204 205 private EasSyncService(String prefix) { 206 super(prefix); 207 } 208 209 public EasSyncService() { 210 this("EAS Validation"); 211 } 212 213 @Override 214 public void alarm() { 215 synchronized(getSynchronizer()) { 216 if (mPendingPost != null) { 217 URI uri = mPendingPost.getURI(); 218 if (uri != null) { 219 String query = uri.getQuery(); 220 if (query == null) { 221 query = "POST"; 222 } 223 userLog("Alert, aborting " + query); 224 } else { 225 userLog("Alert, no URI?"); 226 } 227 mPostAborted = true; 228 mPendingPost.abort(); 229 } else { 230 userLog("Alert, no pending POST"); 231 } 232 } 233 } 234 235 @Override 236 public void reset() { 237 synchronized(getSynchronizer()) { 238 if (mPendingPost != null) { 239 URI uri = mPendingPost.getURI(); 240 if (uri != null) { 241 String query = uri.getQuery(); 242 if (query.startsWith("Cmd=Ping")) { 243 userLog("Reset, aborting Ping"); 244 mPostReset = true; 245 mPendingPost.abort(); 246 } 247 } 248 } 249 } 250 } 251 252 @Override 253 public void stop() { 254 mStop = true; 255 synchronized(getSynchronizer()) { 256 if (mPendingPost != null) { 257 mPendingPost.abort(); 258 } 259 } 260 } 261 262 /** 263 * Determine whether an HTTP code represents an authentication error 264 * @param code the HTTP code returned by the server 265 * @return whether or not the code represents an authentication error 266 */ 267 protected boolean isAuthError(int code) { 268 return (code == HttpStatus.SC_UNAUTHORIZED) || (code == HttpStatus.SC_FORBIDDEN); 269 } 270 271 /** 272 * Determine whether an HTTP code represents a provisioning error 273 * @param code the HTTP code returned by the server 274 * @return whether or not the code represents an provisioning error 275 */ 276 protected boolean isProvisionError(int code) { 277 return (code == HTTP_NEED_PROVISIONING) || (code == HttpStatus.SC_FORBIDDEN); 278 } 279 280 private void setupProtocolVersion(EasSyncService service, Header versionHeader) { 281 String versions = versionHeader.getValue(); 282 if (versions != null) { 283 if (versions.contains("12.0")) { 284 service.mProtocolVersion = "12.0"; 285 } 286 service.mProtocolVersionDouble = Double.parseDouble(service.mProtocolVersion); 287 if (service.mAccount != null) { 288 service.mAccount.mProtocolVersion = service.mProtocolVersion; 289 } 290 } 291 } 292 293 @Override 294 public void validateAccount(String hostAddress, String userName, String password, int port, 295 boolean ssl, boolean trustCertificates, Context context) throws MessagingException { 296 try { 297 userLog("Testing EAS: ", hostAddress, ", ", userName, ", ssl = ", ssl ? "1" : "0"); 298 EasSyncService svc = new EasSyncService("%TestAccount%"); 299 svc.mContext = context; 300 svc.mHostAddress = hostAddress; 301 svc.mUserName = userName; 302 svc.mPassword = password; 303 svc.mSsl = ssl; 304 svc.mTrustSsl = trustCertificates; 305 // We mustn't use the "real" device id or we'll screw up current accounts 306 // Any string will do, but we'll go for "validate" 307 svc.mDeviceId = "validate"; 308 HttpResponse resp = svc.sendHttpClientOptions(); 309 int code = resp.getStatusLine().getStatusCode(); 310 userLog("Validation (OPTIONS) response: " + code); 311 if (code == HttpStatus.SC_OK) { 312 // No exception means successful validation 313 Header commands = resp.getFirstHeader("MS-ASProtocolCommands"); 314 Header versions = resp.getFirstHeader("ms-asprotocolversions"); 315 if (commands == null || versions == null) { 316 userLog("OPTIONS response without commands or versions; reporting I/O error"); 317 throw new MessagingException(MessagingException.IOERROR); 318 } 319 320 // Make sure we've got the right protocol version set up 321 setupProtocolVersion(svc, versions); 322 323 // Run second test here for provisioning failures... 324 Serializer s = new Serializer(); 325 userLog("Try folder sync"); 326 s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY).text("0") 327 .end().end().done(); 328 resp = svc.sendHttpClientPost("FolderSync", s.toByteArray()); 329 code = resp.getStatusLine().getStatusCode(); 330 // We'll get one of the following responses if policies are required by the server 331 if (code == HttpStatus.SC_FORBIDDEN || code == HTTP_NEED_PROVISIONING) { 332 // Get the policies and see if we are able to support them 333 if (svc.canProvision() != null) { 334 // If so, send the advisory Exception (the account may be created later) 335 throw new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED); 336 } else 337 // If not, send the unsupported Exception (the account won't be created) 338 throw new MessagingException( 339 MessagingException.SECURITY_POLICIES_UNSUPPORTED); 340 } 341 userLog("Validation successful"); 342 return; 343 } 344 if (isAuthError(code)) { 345 userLog("Authentication failed"); 346 throw new AuthenticationFailedException("Validation failed"); 347 } else { 348 // TODO Need to catch other kinds of errors (e.g. policy) For now, report the code. 349 userLog("Validation failed, reporting I/O error: ", code); 350 throw new MessagingException(MessagingException.IOERROR); 351 } 352 } catch (IOException e) { 353 Throwable cause = e.getCause(); 354 if (cause != null && cause instanceof CertificateException) { 355 userLog("CertificateException caught: ", e.getMessage()); 356 throw new MessagingException(MessagingException.GENERAL_SECURITY); 357 } 358 userLog("IOException caught: ", e.getMessage()); 359 throw new MessagingException(MessagingException.IOERROR); 360 } 361 362 } 363 364 /** 365 * Gets the redirect location from the HTTP headers and uses that to modify the HttpPost so that 366 * it can be reused 367 * 368 * @param resp the HttpResponse that indicates a redirect (451) 369 * @param post the HttpPost that was originally sent to the server 370 * @return the HttpPost, updated with the redirect location 371 */ 372 private HttpPost getRedirect(HttpResponse resp, HttpPost post) { 373 Header locHeader = resp.getFirstHeader("X-MS-Location"); 374 if (locHeader != null) { 375 String loc = locHeader.getValue(); 376 // If we've gotten one and it shows signs of looking like an address, we try 377 // sending our request there 378 if (loc != null && loc.startsWith("http")) { 379 post.setURI(URI.create(loc)); 380 return post; 381 } 382 } 383 return null; 384 } 385 386 /** 387 * Send the POST command to the autodiscover server, handling a redirect, if necessary, and 388 * return the HttpResponse 389 * 390 * @param client the HttpClient to be used for the request 391 * @param post the HttpPost we're going to send 392 * @return an HttpResponse from the original or redirect server 393 * @throws IOException on any IOException within the HttpClient code 394 * @throws MessagingException 395 */ 396 private HttpResponse postAutodiscover(HttpClient client, HttpPost post) 397 throws IOException, MessagingException { 398 userLog("Posting autodiscover to: " + post.getURI()); 399 HttpResponse resp = client.execute(post); 400 int code = resp.getStatusLine().getStatusCode(); 401 // On a redirect, try the new location 402 if (code == AUTO_DISCOVER_REDIRECT_CODE) { 403 post = getRedirect(resp, post); 404 if (post != null) { 405 userLog("Posting autodiscover to redirect: " + post.getURI()); 406 return client.execute(post); 407 } 408 } else if (code == HttpStatus.SC_UNAUTHORIZED) { 409 // 401 (Unauthorized) is for true auth errors when used in Autodiscover 410 // 403 (and others) we'll just punt on 411 throw new MessagingException(MessagingException.AUTHENTICATION_FAILED); 412 } else if (code != HttpStatus.SC_OK) { 413 // We'll try the next address if this doesn't work 414 userLog("Code: " + code + ", throwing IOException"); 415 throw new IOException(); 416 } 417 return resp; 418 } 419 420 /** 421 * Use the Exchange 2007 AutoDiscover feature to try to retrieve server information using 422 * only an email address and the password 423 * 424 * @param userName the user's email address 425 * @param password the user's password 426 * @return a HostAuth ready to be saved in an Account or null (failure) 427 */ 428 public Bundle tryAutodiscover(String userName, String password) throws RemoteException { 429 XmlSerializer s = Xml.newSerializer(); 430 ByteArrayOutputStream os = new ByteArrayOutputStream(1024); 431 HostAuth hostAuth = new HostAuth(); 432 Bundle bundle = new Bundle(); 433 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, 434 MessagingException.NO_ERROR); 435 try { 436 // Build the XML document that's sent to the autodiscover server(s) 437 s.setOutput(os, "UTF-8"); 438 s.startDocument("UTF-8", false); 439 s.startTag(null, "Autodiscover"); 440 s.attribute(null, "xmlns", AUTO_DISCOVER_SCHEMA_PREFIX + "requestschema/2006"); 441 s.startTag(null, "Request"); 442 s.startTag(null, "EMailAddress").text(userName).endTag(null, "EMailAddress"); 443 s.startTag(null, "AcceptableResponseSchema"); 444 s.text(AUTO_DISCOVER_SCHEMA_PREFIX + "responseschema/2006"); 445 s.endTag(null, "AcceptableResponseSchema"); 446 s.endTag(null, "Request"); 447 s.endTag(null, "Autodiscover"); 448 s.endDocument(); 449 String req = os.toString(); 450 451 // Initialize the user name and password 452 mUserName = userName; 453 mPassword = password; 454 // Make sure the authentication string is created (mAuthString) 455 makeUriString("foo", null); 456 457 // Split out the domain name 458 int amp = userName.indexOf('@'); 459 // The UI ensures that userName is a valid email address 460 if (amp < 0) { 461 throw new RemoteException(); 462 } 463 String domain = userName.substring(amp + 1); 464 465 // There are up to four attempts here; the two URLs that we're supposed to try per the 466 // specification, and up to one redirect for each (handled in postAutodiscover) 467 468 // Try the domain first and see if we can get a response 469 HttpPost post = new HttpPost("https://" + domain + AUTO_DISCOVER_PAGE); 470 setHeaders(post, false); 471 post.setHeader("Content-Type", "text/xml"); 472 post.setEntity(new StringEntity(req)); 473 HttpClient client = getHttpClient(COMMAND_TIMEOUT); 474 HttpResponse resp; 475 try { 476 resp = postAutodiscover(client, post); 477 } catch (ClientProtocolException e1) { 478 return null; 479 } catch (IOException e1) { 480 // We catch the IOException here because we have an alternate address to try 481 post.setURI(URI.create("https://autodiscover." + domain + AUTO_DISCOVER_PAGE)); 482 // If we fail here, we're out of options, so we let the outer try catch the 483 // IOException and return null 484 resp = postAutodiscover(client, post); 485 } 486 487 // Get the "final" code; if it's not 200, just return null 488 int code = resp.getStatusLine().getStatusCode(); 489 userLog("Code: " + code); 490 if (code != HttpStatus.SC_OK) return null; 491 492 // At this point, we have a 200 response (SC_OK) 493 HttpEntity e = resp.getEntity(); 494 InputStream is = e.getContent(); 495 try { 496 // The response to Autodiscover is regular XML (not WBXML) 497 // If we ever get an error in this process, we'll just punt and return null 498 XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); 499 XmlPullParser parser = factory.newPullParser(); 500 parser.setInput(is, "UTF-8"); 501 int type = parser.getEventType(); 502 if (type == XmlPullParser.START_DOCUMENT) { 503 type = parser.next(); 504 if (type == XmlPullParser.START_TAG) { 505 String name = parser.getName(); 506 if (name.equals("Autodiscover")) { 507 hostAuth = new HostAuth(); 508 parseAutodiscover(parser, hostAuth); 509 // On success, we'll have a server address and login 510 if (hostAuth.mAddress != null && hostAuth.mLogin != null) { 511 // Fill in the rest of the HostAuth 512 hostAuth.mPassword = password; 513 hostAuth.mPort = 443; 514 hostAuth.mProtocol = "eas"; 515 hostAuth.mFlags = 516 HostAuth.FLAG_SSL | HostAuth.FLAG_AUTHENTICATE; 517 bundle.putParcelable( 518 EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH, hostAuth); 519 } else { 520 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, 521 MessagingException.UNSPECIFIED_EXCEPTION); 522 } 523 } 524 } 525 } 526 } catch (XmlPullParserException e1) { 527 // This would indicate an I/O error of some sort 528 // We will simply return null and user can configure manually 529 } 530 // There's no reason at all for exceptions to be thrown, and it's ok if so. 531 // We just won't do auto-discover; user can configure manually 532 } catch (IllegalArgumentException e) { 533 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, 534 MessagingException.UNSPECIFIED_EXCEPTION); 535 } catch (IllegalStateException e) { 536 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, 537 MessagingException.UNSPECIFIED_EXCEPTION); 538 } catch (IOException e) { 539 userLog("IOException in Autodiscover", e); 540 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, 541 MessagingException.IOERROR); 542 } catch (MessagingException e) { 543 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, 544 MessagingException.AUTHENTICATION_FAILED); 545 } 546 return bundle; 547 } 548 549 void parseServer(XmlPullParser parser, HostAuth hostAuth) 550 throws XmlPullParserException, IOException { 551 boolean mobileSync = false; 552 while (true) { 553 int type = parser.next(); 554 if (type == XmlPullParser.END_TAG && parser.getName().equals("Server")) { 555 break; 556 } else if (type == XmlPullParser.START_TAG) { 557 String name = parser.getName(); 558 if (name.equals("Type")) { 559 if (parser.nextText().equals("MobileSync")) { 560 mobileSync = true; 561 } 562 } else if (mobileSync && name.equals("Url")) { 563 String url = parser.nextText().toLowerCase(); 564 // This will look like https://<server address>/Microsoft-Server-ActiveSync 565 // We need to extract the <server address> 566 if (url.startsWith("https://") && 567 url.endsWith("/microsoft-server-activesync")) { 568 int lastSlash = url.lastIndexOf('/'); 569 hostAuth.mAddress = url.substring(8, lastSlash); 570 userLog("Autodiscover, server: " + hostAuth.mAddress); 571 } 572 } 573 } 574 } 575 } 576 577 void parseSettings(XmlPullParser parser, HostAuth hostAuth) 578 throws XmlPullParserException, IOException { 579 while (true) { 580 int type = parser.next(); 581 if (type == XmlPullParser.END_TAG && parser.getName().equals("Settings")) { 582 break; 583 } else if (type == XmlPullParser.START_TAG) { 584 String name = parser.getName(); 585 if (name.equals("Server")) { 586 parseServer(parser, hostAuth); 587 } 588 } 589 } 590 } 591 592 void parseAction(XmlPullParser parser, HostAuth hostAuth) 593 throws XmlPullParserException, IOException { 594 while (true) { 595 int type = parser.next(); 596 if (type == XmlPullParser.END_TAG && parser.getName().equals("Action")) { 597 break; 598 } else if (type == XmlPullParser.START_TAG) { 599 String name = parser.getName(); 600 if (name.equals("Error")) { 601 // Should parse the error 602 } else if (name.equals("Redirect")) { 603 Log.d(TAG, "Redirect: " + parser.nextText()); 604 } else if (name.equals("Settings")) { 605 parseSettings(parser, hostAuth); 606 } 607 } 608 } 609 } 610 611 void parseUser(XmlPullParser parser, HostAuth hostAuth) 612 throws XmlPullParserException, IOException { 613 while (true) { 614 int type = parser.next(); 615 if (type == XmlPullParser.END_TAG && parser.getName().equals("User")) { 616 break; 617 } else if (type == XmlPullParser.START_TAG) { 618 String name = parser.getName(); 619 if (name.equals("EMailAddress")) { 620 String addr = parser.nextText(); 621 hostAuth.mLogin = addr; 622 userLog("Autodiscover, login: " + addr); 623 } else if (name.equals("DisplayName")) { 624 String dn = parser.nextText(); 625 userLog("Autodiscover, user: " + dn); 626 } 627 } 628 } 629 } 630 631 void parseResponse(XmlPullParser parser, HostAuth hostAuth) 632 throws XmlPullParserException, IOException { 633 while (true) { 634 int type = parser.next(); 635 if (type == XmlPullParser.END_TAG && parser.getName().equals("Response")) { 636 break; 637 } else if (type == XmlPullParser.START_TAG) { 638 String name = parser.getName(); 639 if (name.equals("User")) { 640 parseUser(parser, hostAuth); 641 } else if (name.equals("Action")) { 642 parseAction(parser, hostAuth); 643 } 644 } 645 } 646 } 647 648 void parseAutodiscover(XmlPullParser parser, HostAuth hostAuth) 649 throws XmlPullParserException, IOException { 650 while (true) { 651 int type = parser.nextTag(); 652 if (type == XmlPullParser.END_TAG && parser.getName().equals("Autodiscover")) { 653 break; 654 } else if (type == XmlPullParser.START_TAG && parser.getName().equals("Response")) { 655 parseResponse(parser, hostAuth); 656 } 657 } 658 } 659 660 private void doStatusCallback(long messageId, long attachmentId, int status) { 661 try { 662 SyncManager.callback().loadAttachmentStatus(messageId, attachmentId, status, 0); 663 } catch (RemoteException e) { 664 // No danger if the client is no longer around 665 } 666 } 667 668 private void doProgressCallback(long messageId, long attachmentId, int progress) { 669 try { 670 SyncManager.callback().loadAttachmentStatus(messageId, attachmentId, 671 EmailServiceStatus.IN_PROGRESS, progress); 672 } catch (RemoteException e) { 673 // No danger if the client is no longer around 674 } 675 } 676 677 public File createUniqueFileInternal(String dir, String filename) { 678 File directory; 679 if (dir == null) { 680 directory = mContext.getFilesDir(); 681 } else { 682 directory = new File(dir); 683 } 684 if (!directory.exists()) { 685 directory.mkdirs(); 686 } 687 File file = new File(directory, filename); 688 if (!file.exists()) { 689 return file; 690 } 691 // Get the extension of the file, if any. 692 int index = filename.lastIndexOf('.'); 693 String name = filename; 694 String extension = ""; 695 if (index != -1) { 696 name = filename.substring(0, index); 697 extension = filename.substring(index); 698 } 699 for (int i = 2; i < Integer.MAX_VALUE; i++) { 700 file = new File(directory, name + '-' + i + extension); 701 if (!file.exists()) { 702 return file; 703 } 704 } 705 return null; 706 } 707 708 /** 709 * Loads an attachment, based on the PartRequest passed in. The PartRequest is basically our 710 * wrapper for Attachment 711 * @param req the part (attachment) to be retrieved 712 * @throws IOException 713 */ 714 protected void getAttachment(PartRequest req) throws IOException { 715 Attachment att = req.mAttachment; 716 Message msg = Message.restoreMessageWithId(mContext, att.mMessageKey); 717 doProgressCallback(msg.mId, att.mId, 0); 718 719 String cmd = "GetAttachment&AttachmentName=" + att.mLocation; 720 HttpResponse res = sendHttpClientPost(cmd, null, COMMAND_TIMEOUT); 721 722 int status = res.getStatusLine().getStatusCode(); 723 if (status == HttpStatus.SC_OK) { 724 HttpEntity e = res.getEntity(); 725 int len = (int)e.getContentLength(); 726 InputStream is = res.getEntity().getContent(); 727 File f = (req.mDestination != null) 728 ? new File(req.mDestination) 729 : createUniqueFileInternal(req.mDestination, att.mFileName); 730 if (f != null) { 731 // Ensure that the target directory exists 732 File destDir = f.getParentFile(); 733 if (!destDir.exists()) { 734 destDir.mkdirs(); 735 } 736 FileOutputStream os = new FileOutputStream(f); 737 // len > 0 means that Content-Length was set in the headers 738 // len < 0 means "chunked" transfer-encoding 739 if (len != 0) { 740 try { 741 mPendingRequest = req; 742 byte[] bytes = new byte[CHUNK_SIZE]; 743 int length = len; 744 // Loop terminates 1) when EOF is reached or 2) if an IOException occurs 745 // One of these is guaranteed to occur 746 int totalRead = 0; 747 userLog("Attachment content-length: ", len); 748 while (true) { 749 int read = is.read(bytes, 0, CHUNK_SIZE); 750 751 // read < 0 means that EOF was reached 752 if (read < 0) { 753 userLog("Attachment load reached EOF, totalRead: ", totalRead); 754 break; 755 } 756 757 // Keep track of how much we've read for progress callback 758 totalRead += read; 759 760 // Write these bytes out 761 os.write(bytes, 0, read); 762 763 // We can't report percentages if this is chunked; by definition, the 764 // length of incoming data is unknown 765 if (length > 0) { 766 // Belt and suspenders check to prevent runaway reading 767 if (totalRead > length) { 768 errorLog("totalRead is greater than attachment length?"); 769 break; 770 } 771 int pct = (totalRead * 100) / length; 772 doProgressCallback(msg.mId, att.mId, pct); 773 } 774 } 775 } finally { 776 mPendingRequest = null; 777 } 778 } 779 os.flush(); 780 os.close(); 781 782 // EmailProvider will throw an exception if we try to update an unsaved attachment 783 if (att.isSaved()) { 784 String contentUriString = (req.mContentUriString != null) 785 ? req.mContentUriString 786 : "file://" + f.getAbsolutePath(); 787 ContentValues cv = new ContentValues(); 788 cv.put(AttachmentColumns.CONTENT_URI, contentUriString); 789 att.update(mContext, cv); 790 doStatusCallback(msg.mId, att.mId, EmailServiceStatus.SUCCESS); 791 } 792 } 793 } else { 794 doStatusCallback(msg.mId, att.mId, EmailServiceStatus.MESSAGE_NOT_FOUND); 795 } 796 } 797 798 /** 799 * Send an email responding to a Message that has been marked as a meeting request. The message 800 * will consist a little bit of event information and an iCalendar attachment 801 * @param msg the meeting request email 802 */ 803 private void sendMeetingResponseMail(Message msg) { 804 // Get the meeting information; we'd better have some... 805 PackedString meetingInfo = new PackedString(msg.mMeetingInfo); 806 if (meetingInfo == null) return; 807 808 // This will come as "First Last" <box@server.blah>, so we use Address to 809 // parse it into parts; we only need the email address part for the ics file 810 Address[] addrs = Address.parse(meetingInfo.get(MeetingInfo.MEETING_ORGANIZER_EMAIL)); 811 // It shouldn't be possible, but handle it anyway 812 if (addrs.length != 1) return; 813 String organizerEmail = addrs[0].getAddress(); 814 815 String dtStamp = meetingInfo.get(MeetingInfo.MEETING_DTSTAMP); 816 String dtStart = meetingInfo.get(MeetingInfo.MEETING_DTSTART); 817 String dtEnd = meetingInfo.get(MeetingInfo.MEETING_DTEND); 818 819 // What we're doing here is to create an Entity that looks like an Event as it would be 820 // stored by CalendarProvider 821 ContentValues entityValues = new ContentValues(); 822 Entity entity = new Entity(entityValues); 823 824 // Fill in times, location, title, and organizer 825 entityValues.put("DTSTAMP", 826 CalendarUtilities.convertEmailDateTimeToCalendarDateTime(dtStamp)); 827 entityValues.put(Events.DTSTART, Utility.parseEmailDateTimeToMillis(dtStart)); 828 entityValues.put(Events.DTEND, Utility.parseEmailDateTimeToMillis(dtEnd)); 829 entityValues.put(Events.EVENT_LOCATION, meetingInfo.get(MeetingInfo.MEETING_LOCATION)); 830 entityValues.put(Events.TITLE, meetingInfo.get(MeetingInfo.MEETING_TITLE)); 831 entityValues.put(Events.ORGANIZER, organizerEmail); 832 833 // Add ourselves as an attendee, using our account email address 834 ContentValues attendeeValues = new ContentValues(); 835 attendeeValues.put(Attendees.ATTENDEE_RELATIONSHIP, 836 Attendees.RELATIONSHIP_ATTENDEE); 837 attendeeValues.put(Attendees.ATTENDEE_EMAIL, mAccount.mEmailAddress); 838 entity.addSubValue(Attendees.CONTENT_URI, attendeeValues); 839 840 // Add the organizer 841 ContentValues organizerValues = new ContentValues(); 842 organizerValues.put(Attendees.ATTENDEE_RELATIONSHIP, 843 Attendees.RELATIONSHIP_ORGANIZER); 844 organizerValues.put(Attendees.ATTENDEE_EMAIL, organizerEmail); 845 entity.addSubValue(Attendees.CONTENT_URI, organizerValues); 846 847 // Create a message from the Entity we've built. The message will have fields like 848 // to, subject, date, and text filled in. There will also be an "inline" attachment 849 // which is in iCalendar format 850 Message outgoingMsg = 851 CalendarUtilities.createMessageForEntity(mContext, entity, 852 Message.FLAG_OUTGOING_MEETING_ACCEPT, 853 meetingInfo.get(MeetingInfo.MEETING_UID), mAccount); 854 // Assuming we got a message back (we might not if the event has been deleted), send it 855 if (outgoingMsg != null) { 856 EasOutboxService.sendMessage(mContext, mAccount.mId, outgoingMsg); 857 } 858 } 859 860 /** 861 * Responds to a meeting request. The MeetingResponseRequest is basically our 862 * wrapper for the meetingResponse service call 863 * @param req the request (message id and response code) 864 * @throws IOException 865 */ 866 protected void sendMeetingResponse(MeetingResponseRequest req) throws IOException { 867 // Retrieve the message and mailbox; punt if either are null 868 Message msg = Message.restoreMessageWithId(mContext, req.mMessageId); 869 if (msg == null) return; 870 Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, msg.mMailboxKey); 871 if (mailbox == null) return; 872 Serializer s = new Serializer(); 873 s.start(Tags.MREQ_MEETING_RESPONSE).start(Tags.MREQ_REQUEST); 874 s.data(Tags.MREQ_USER_RESPONSE, Integer.toString(req.mResponse)); 875 s.data(Tags.MREQ_COLLECTION_ID, mailbox.mServerId); 876 s.data(Tags.MREQ_REQ_ID, msg.mServerId); 877 s.end().end().done(); 878 HttpResponse res = sendHttpClientPost("MeetingResponse", s.toByteArray()); 879 int status = res.getStatusLine().getStatusCode(); 880 if (status == HttpStatus.SC_OK) { 881 HttpEntity e = res.getEntity(); 882 int len = (int)e.getContentLength(); 883 InputStream is = res.getEntity().getContent(); 884 if (len != 0) { 885 new MeetingResponseParser(is, this).parse(); 886 sendMeetingResponseMail(msg); 887 } 888 } else if (isAuthError(status)) { 889 throw new EasAuthenticationException(); 890 } else { 891 userLog("Meeting response request failed, code: " + status); 892 throw new IOException(); 893 } 894 } 895 896 @SuppressWarnings("deprecation") 897 private String makeUriString(String cmd, String extra) throws IOException { 898 // Cache the authentication string and the command string 899 String safeUserName = URLEncoder.encode(mUserName); 900 if (mAuthString == null) { 901 String cs = mUserName + ':' + mPassword; 902 mAuthString = "Basic " + Base64.encodeToString(cs.getBytes(), Base64.NO_WRAP); 903 mCmdString = "&User=" + safeUserName + "&DeviceId=" + mDeviceId + "&DeviceType=" 904 + mDeviceType; 905 } 906 String us = (mSsl ? (mTrustSsl ? "httpts" : "https") : "http") + "://" + mHostAddress + 907 "/Microsoft-Server-ActiveSync"; 908 if (cmd != null) { 909 us += "?Cmd=" + cmd + mCmdString; 910 } 911 if (extra != null) { 912 us += extra; 913 } 914 return us; 915 } 916 917 /** 918 * Set standard HTTP headers, using a policy key if required 919 * @param method the method we are going to send 920 * @param usePolicyKey whether or not a policy key should be sent in the headers 921 */ 922 private void setHeaders(HttpRequestBase method, boolean usePolicyKey) { 923 method.setHeader("Authorization", mAuthString); 924 method.setHeader("MS-ASProtocolVersion", mProtocolVersion); 925 method.setHeader("Connection", "keep-alive"); 926 method.setHeader("User-Agent", mDeviceType + '/' + Eas.VERSION); 927 if (usePolicyKey && (mAccount != null)) { 928 String key = mAccount.mSecuritySyncKey; 929 if (key == null || key.length() == 0) { 930 return; 931 } 932 method.setHeader("X-MS-PolicyKey", key); 933 } 934 } 935 936 private ClientConnectionManager getClientConnectionManager() { 937 return SyncManager.getClientConnectionManager(); 938 } 939 940 private HttpClient getHttpClient(int timeout) { 941 HttpParams params = new BasicHttpParams(); 942 HttpConnectionParams.setConnectionTimeout(params, 15*SECONDS); 943 HttpConnectionParams.setSoTimeout(params, timeout); 944 HttpConnectionParams.setSocketBufferSize(params, 8192); 945 HttpClient client = new DefaultHttpClient(getClientConnectionManager(), params); 946 return client; 947 } 948 949 protected HttpResponse sendHttpClientPost(String cmd, byte[] bytes) throws IOException { 950 return sendHttpClientPost(cmd, new ByteArrayEntity(bytes), COMMAND_TIMEOUT); 951 } 952 953 protected HttpResponse sendHttpClientPost(String cmd, HttpEntity entity) throws IOException { 954 return sendHttpClientPost(cmd, entity, COMMAND_TIMEOUT); 955 } 956 957 protected HttpResponse sendPing(byte[] bytes, int heartbeat) throws IOException { 958 Thread.currentThread().setName(mAccount.mDisplayName + ": Ping"); 959 if (Eas.USER_LOG) { 960 userLog("Send ping, timeout: " + heartbeat + "s, high: " + mPingHighWaterMark + 's'); 961 } 962 return sendHttpClientPost(PING_COMMAND, new ByteArrayEntity(bytes), (heartbeat+5)*SECONDS); 963 } 964 965 protected HttpResponse sendHttpClientPost(String cmd, HttpEntity entity, int timeout) 966 throws IOException { 967 HttpClient client = getHttpClient(timeout); 968 boolean isPingCommand = cmd.equals(PING_COMMAND); 969 970 // Split the mail sending commands 971 String extra = null; 972 boolean msg = false; 973 if (cmd.startsWith("SmartForward&") || cmd.startsWith("SmartReply&")) { 974 int cmdLength = cmd.indexOf('&'); 975 extra = cmd.substring(cmdLength); 976 cmd = cmd.substring(0, cmdLength); 977 msg = true; 978 } else if (cmd.startsWith("SendMail&")) { 979 msg = true; 980 } 981 982 String us = makeUriString(cmd, extra); 983 HttpPost method = new HttpPost(URI.create(us)); 984 // Send the proper Content-Type header 985 // If entity is null (e.g. for attachments), don't set this header 986 if (msg) { 987 method.setHeader("Content-Type", "message/rfc822"); 988 } else if (entity != null) { 989 method.setHeader("Content-Type", "application/vnd.ms-sync.wbxml"); 990 } 991 setHeaders(method, !cmd.equals(PING_COMMAND)); 992 method.setEntity(entity); 993 synchronized(getSynchronizer()) { 994 mPendingPost = method; 995 long alarmTime = timeout+(10*SECONDS); 996 if (isPingCommand) { 997 SyncManager.runAsleep(mMailboxId, alarmTime); 998 } else { 999 SyncManager.setWatchdogAlarm(mMailboxId, alarmTime); 1000 } 1001 } 1002 try { 1003 return client.execute(method); 1004 } finally { 1005 synchronized(getSynchronizer()) { 1006 if (isPingCommand) { 1007 SyncManager.runAwake(mMailboxId); 1008 } else { 1009 SyncManager.clearWatchdogAlarm(mMailboxId); 1010 } 1011 mPendingPost = null; 1012 } 1013 } 1014 } 1015 1016 protected HttpResponse sendHttpClientOptions() throws IOException { 1017 HttpClient client = getHttpClient(COMMAND_TIMEOUT); 1018 String us = makeUriString("OPTIONS", null); 1019 HttpOptions method = new HttpOptions(URI.create(us)); 1020 setHeaders(method, false); 1021 return client.execute(method); 1022 } 1023 1024 String getTargetCollectionClassFromCursor(Cursor c) { 1025 int type = c.getInt(Mailbox.CONTENT_TYPE_COLUMN); 1026 if (type == Mailbox.TYPE_CONTACTS) { 1027 return "Contacts"; 1028 } else if (type == Mailbox.TYPE_CALENDAR) { 1029 return "Calendar"; 1030 } else { 1031 return "Email"; 1032 } 1033 } 1034 1035 /** 1036 * Negotiate provisioning with the server. First, get policies form the server and see if 1037 * the policies are supported by the device. Then, write the policies to the account and 1038 * tell SecurityPolicy that we have policies in effect. Finally, see if those policies are 1039 * active; if so, acknowledge the policies to the server and get a final policy key that we 1040 * use in future EAS commands and write this key to the account. 1041 * @return whether or not provisioning has been successful 1042 * @throws IOException 1043 */ 1044 private boolean tryProvision() throws IOException { 1045 // First, see if provisioning is even possible, i.e. do we support the policies required 1046 // by the server 1047 ProvisionParser pp = canProvision(); 1048 if (pp != null) { 1049 SecurityPolicy sp = SecurityPolicy.getInstance(mContext); 1050 // Get the policies from ProvisionParser 1051 PolicySet ps = pp.getPolicySet(); 1052 // Update the account with a null policyKey (the key we've gotten is 1053 // temporary and cannot be used for syncing) 1054 if (ps.writeAccount(mAccount, null, true, mContext)) { 1055 sp.updatePolicies(mAccount.mId); 1056 } 1057 if (pp.getRemoteWipe()) { 1058 // We've gotten a remote wipe command 1059 // First, we've got to acknowledge it, but wrap the wipe in try/catch so that 1060 // we wipe the device regardless of any errors in acknowledgment 1061 try { 1062 acknowledgeRemoteWipe(pp.getPolicyKey()); 1063 } catch (Exception e) { 1064 // Because remote wipe is such a high priority task, we don't want to 1065 // circumvent it if there's an exception in acknowledgment 1066 } 1067 // Then, tell SecurityPolicy to wipe the device 1068 sp.remoteWipe(); 1069 return false; 1070 } else if (sp.isActive(ps)) { 1071 // See if the required policies are in force; if they are, acknowledge the policies 1072 // to the server and get the final policy key 1073 String policyKey = acknowledgeProvision(pp.getPolicyKey()); 1074 if (policyKey != null) { 1075 // Write the final policy key to the Account and say we've been successful 1076 ps.writeAccount(mAccount, policyKey, true, mContext); 1077 return true; 1078 } 1079 } else { 1080 // Notify that we are blocked because of policies 1081 sp.policiesRequired(mAccount.mId); 1082 } 1083 } 1084 return false; 1085 } 1086 1087 private String getPolicyType() { 1088 return (mProtocolVersionDouble >= 12.0) ? EAS_12_POLICY_TYPE : EAS_2_POLICY_TYPE; 1089 } 1090 1091 // TODO This is Exchange 2007 only at this point 1092 /** 1093 * Obtain a set of policies from the server and determine whether those policies are supported 1094 * by the device. 1095 * @return the ProvisionParser (holds policies and key) if we receive policies and they are 1096 * supported by the device; null otherwise 1097 * @throws IOException 1098 */ 1099 private ProvisionParser canProvision() throws IOException { 1100 Serializer s = new Serializer(); 1101 s.start(Tags.PROVISION_PROVISION).start(Tags.PROVISION_POLICIES); 1102 s.start(Tags.PROVISION_POLICY).data(Tags.PROVISION_POLICY_TYPE, getPolicyType()) 1103 .end().end().end().done(); 1104 HttpResponse resp = sendHttpClientPost("Provision", s.toByteArray()); 1105 int code = resp.getStatusLine().getStatusCode(); 1106 if (code == HttpStatus.SC_OK) { 1107 InputStream is = resp.getEntity().getContent(); 1108 ProvisionParser pp = new ProvisionParser(is, this); 1109 if (pp.parse()) { 1110 // If true, we received policies from the server; see if they are supported by 1111 // the framework; if so, return the ProvisionParser containing the policy set and 1112 // temporary key 1113 PolicySet ps = pp.getPolicySet(); 1114 if (SecurityPolicy.getInstance(mContext).isSupported(ps)) { 1115 return pp; 1116 } 1117 } 1118 } 1119 // On failures, simply return null 1120 return null; 1121 } 1122 1123 // TODO This is Exchange 2007 only at this point 1124 /** 1125 * Acknowledge that we support the policies provided by the server, and that these policies 1126 * are in force. 1127 * @param tempKey the initial (temporary) policy key sent by the server 1128 * @return the final policy key, which can be used for syncing 1129 * @throws IOException 1130 */ 1131 private void acknowledgeRemoteWipe(String tempKey) throws IOException { 1132 acknowledgeProvisionImpl(tempKey, true); 1133 } 1134 1135 private String acknowledgeProvision(String tempKey) throws IOException { 1136 return acknowledgeProvisionImpl(tempKey, false); 1137 } 1138 1139 private String acknowledgeProvisionImpl(String tempKey, boolean remoteWipe) throws IOException { 1140 Serializer s = new Serializer(); 1141 s.start(Tags.PROVISION_PROVISION).start(Tags.PROVISION_POLICIES); 1142 s.start(Tags.PROVISION_POLICY); 1143 1144 // Use the proper policy type, depending on EAS version 1145 s.data(Tags.PROVISION_POLICY_TYPE, getPolicyType()); 1146 1147 s.data(Tags.PROVISION_POLICY_KEY, tempKey); 1148 s.data(Tags.PROVISION_STATUS, "1"); 1149 if (remoteWipe) { 1150 s.start(Tags.PROVISION_REMOTE_WIPE); 1151 s.data(Tags.PROVISION_STATUS, "1"); 1152 s.end(); 1153 } 1154 s.end(); // PROVISION_POLICY 1155 s.end().end().done(); // PROVISION_POLICIES, PROVISION_PROVISION 1156 HttpResponse resp = sendHttpClientPost("Provision", s.toByteArray()); 1157 int code = resp.getStatusLine().getStatusCode(); 1158 if (code == HttpStatus.SC_OK) { 1159 InputStream is = resp.getEntity().getContent(); 1160 ProvisionParser pp = new ProvisionParser(is, this); 1161 if (pp.parse()) { 1162 // Return the final polic key from the ProvisionParser 1163 return pp.getPolicyKey(); 1164 } 1165 } 1166 // On failures, return null 1167 return null; 1168 } 1169 1170 /** 1171 * Performs FolderSync 1172 * 1173 * @throws IOException 1174 * @throws EasParserException 1175 */ 1176 public void runAccountMailbox() throws IOException, EasParserException { 1177 // Initialize exit status to success 1178 mExitStatus = EmailServiceStatus.SUCCESS; 1179 try { 1180 try { 1181 SyncManager.callback() 1182 .syncMailboxListStatus(mAccount.mId, EmailServiceStatus.IN_PROGRESS, 0); 1183 } catch (RemoteException e1) { 1184 // Don't care if this fails 1185 } 1186 1187 if (mAccount.mSyncKey == null) { 1188 mAccount.mSyncKey = "0"; 1189 userLog("Account syncKey INIT to 0"); 1190 ContentValues cv = new ContentValues(); 1191 cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey); 1192 mAccount.update(mContext, cv); 1193 } 1194 1195 boolean firstSync = mAccount.mSyncKey.equals("0"); 1196 if (firstSync) { 1197 userLog("Initial FolderSync"); 1198 } 1199 1200 // When we first start up, change all mailboxes to push. 1201 ContentValues cv = new ContentValues(); 1202 cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH); 1203 if (mContentResolver.update(Mailbox.CONTENT_URI, cv, 1204 WHERE_ACCOUNT_AND_SYNC_INTERVAL_PING, 1205 new String[] {Long.toString(mAccount.mId)}) > 0) { 1206 SyncManager.kick("change ping boxes to push"); 1207 } 1208 1209 // Determine our protocol version, if we haven't already and save it in the Account 1210 // Also re-check protocol version at least once a day (in case of upgrade) 1211 if (mAccount.mProtocolVersion == null || 1212 ((System.currentTimeMillis() - mMailbox.mSyncTime) > DAYS)) { 1213 userLog("Determine EAS protocol version"); 1214 HttpResponse resp = sendHttpClientOptions(); 1215 int code = resp.getStatusLine().getStatusCode(); 1216 userLog("OPTIONS response: ", code); 1217 if (code == HttpStatus.SC_OK) { 1218 Header header = resp.getFirstHeader("MS-ASProtocolCommands"); 1219 userLog(header.getValue()); 1220 header = resp.getFirstHeader("ms-asprotocolversions"); 1221 String versions = header.getValue(); 1222 if (versions != null) { 1223 if (versions.contains("12.0")) { 1224 mProtocolVersion = "12.0"; 1225 } 1226 mProtocolVersionDouble = Double.parseDouble(mProtocolVersion); 1227 mAccount.mProtocolVersion = mProtocolVersion; 1228 // Save the protocol version 1229 cv.clear(); 1230 // Save the protocol version in the account 1231 cv.put(Account.PROTOCOL_VERSION, mProtocolVersion); 1232 mAccount.update(mContext, cv); 1233 cv.clear(); 1234 // Save the sync time of the account mailbox to current time 1235 cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis()); 1236 mMailbox.update(mContext, cv); 1237 userLog(versions); 1238 userLog("Using version ", mProtocolVersion); 1239 } else { 1240 errorLog("No protocol versions in OPTIONS response"); 1241 throw new IOException(); 1242 } 1243 } else { 1244 errorLog("OPTIONS command failed; throwing IOException"); 1245 throw new IOException(); 1246 } 1247 } 1248 1249 // Change all pushable boxes to push when we start the account mailbox 1250 if (mAccount.mSyncInterval == Account.CHECK_INTERVAL_PUSH) { 1251 cv.clear(); 1252 cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH); 1253 if (mContentResolver.update(Mailbox.CONTENT_URI, cv, 1254 SyncManager.WHERE_IN_ACCOUNT_AND_PUSHABLE, 1255 new String[] {Long.toString(mAccount.mId)}) > 0) { 1256 userLog("Push account; set pushable boxes to push..."); 1257 } 1258 } 1259 1260 while (!mStop) { 1261 userLog("Sending Account syncKey: ", mAccount.mSyncKey); 1262 Serializer s = new Serializer(); 1263 s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY) 1264 .text(mAccount.mSyncKey).end().end().done(); 1265 HttpResponse resp = sendHttpClientPost("FolderSync", s.toByteArray()); 1266 if (mStop) break; 1267 int code = resp.getStatusLine().getStatusCode(); 1268 if (code == HttpStatus.SC_OK) { 1269 HttpEntity entity = resp.getEntity(); 1270 int len = (int)entity.getContentLength(); 1271 if (len != 0) { 1272 InputStream is = entity.getContent(); 1273 // Returns true if we need to sync again 1274 if (new FolderSyncParser(is, new AccountSyncAdapter(mMailbox, this)) 1275 .parse()) { 1276 continue; 1277 } 1278 } 1279 } else if (isProvisionError(code)) { 1280 // If the sync error is a provisioning failure (perhaps the policies changed), 1281 // let's try the provisining procedure 1282 if (!tryProvision()) { 1283 // Set the appropriate failure status 1284 mExitStatus = EXIT_SECURITY_FAILURE; 1285 return; 1286 } else { 1287 // If we succeeded, try again... 1288 continue; 1289 } 1290 } else if (isAuthError(code)) { 1291 mExitStatus = EXIT_LOGIN_FAILURE; 1292 return; 1293 } else { 1294 userLog("FolderSync response error: ", code); 1295 } 1296 1297 // Change all push/hold boxes to push 1298 cv.clear(); 1299 cv.put(Mailbox.SYNC_INTERVAL, Account.CHECK_INTERVAL_PUSH); 1300 if (mContentResolver.update(Mailbox.CONTENT_URI, cv, 1301 WHERE_PUSH_HOLD_NOT_ACCOUNT_MAILBOX, 1302 new String[] {Long.toString(mAccount.mId)}) > 0) { 1303 userLog("Set push/hold boxes to push..."); 1304 } 1305 1306 try { 1307 SyncManager.callback() 1308 .syncMailboxListStatus(mAccount.mId, mExitStatus, 0); 1309 } catch (RemoteException e1) { 1310 // Don't care if this fails 1311 } 1312 1313 // Wait for push notifications. 1314 String threadName = Thread.currentThread().getName(); 1315 try { 1316 runPingLoop(); 1317 } catch (StaleFolderListException e) { 1318 // We break out if we get told about a stale folder list 1319 userLog("Ping interrupted; folder list requires sync..."); 1320 } finally { 1321 Thread.currentThread().setName(threadName); 1322 } 1323 } 1324 } catch (IOException e) { 1325 // We catch this here to send the folder sync status callback 1326 // A folder sync failed callback will get sent from run() 1327 try { 1328 if (!mStop) { 1329 SyncManager.callback() 1330 .syncMailboxListStatus(mAccount.mId, 1331 EmailServiceStatus.CONNECTION_ERROR, 0); 1332 } 1333 } catch (RemoteException e1) { 1334 // Don't care if this fails 1335 } 1336 throw e; 1337 } 1338 } 1339 1340 void pushFallback(long mailboxId) { 1341 Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId); 1342 ContentValues cv = new ContentValues(); 1343 int mins = PING_FALLBACK_PIM; 1344 if (mailbox.mType == Mailbox.TYPE_INBOX) { 1345 mins = PING_FALLBACK_INBOX; 1346 } 1347 cv.put(Mailbox.SYNC_INTERVAL, mins); 1348 mContentResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId), 1349 cv, null, null); 1350 errorLog("*** PING ERROR LOOP: Set " + mailbox.mDisplayName + " to " + mins + " min sync"); 1351 SyncManager.kick("push fallback"); 1352 } 1353 1354 void runPingLoop() throws IOException, StaleFolderListException { 1355 int pingHeartbeat = mPingHeartbeat; 1356 userLog("runPingLoop"); 1357 // Do push for all sync services here 1358 long endTime = System.currentTimeMillis() + (30*MINUTES); 1359 HashMap<String, Integer> pingErrorMap = new HashMap<String, Integer>(); 1360 ArrayList<String> readyMailboxes = new ArrayList<String>(); 1361 ArrayList<String> notReadyMailboxes = new ArrayList<String>(); 1362 int pingWaitCount = 0; 1363 1364 while ((System.currentTimeMillis() < endTime) && !mStop) { 1365 // Count of pushable mailboxes 1366 int pushCount = 0; 1367 // Count of mailboxes that can be pushed right now 1368 int canPushCount = 0; 1369 // Count of uninitialized boxes 1370 int uninitCount = 0; 1371 1372 Serializer s = new Serializer(); 1373 Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, 1374 MailboxColumns.ACCOUNT_KEY + '=' + mAccount.mId + 1375 AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX, null, null); 1376 notReadyMailboxes.clear(); 1377 readyMailboxes.clear(); 1378 try { 1379 // Loop through our pushed boxes seeing what is available to push 1380 while (c.moveToNext()) { 1381 pushCount++; 1382 // Two requirements for push: 1383 // 1) SyncManager tells us the mailbox is syncable (not running, not stopped) 1384 // 2) The syncKey isn't "0" (i.e. it's synced at least once) 1385 long mailboxId = c.getLong(Mailbox.CONTENT_ID_COLUMN); 1386 int pingStatus = SyncManager.pingStatus(mailboxId); 1387 String mailboxName = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN); 1388 if (pingStatus == SyncManager.PING_STATUS_OK) { 1389 String syncKey = c.getString(Mailbox.CONTENT_SYNC_KEY_COLUMN); 1390 if ((syncKey == null) || syncKey.equals("0")) { 1391 // We can't push until the initial sync is done 1392 pushCount--; 1393 uninitCount++; 1394 continue; 1395 } 1396 1397 if (canPushCount++ == 0) { 1398 // Initialize the Ping command 1399 s.start(Tags.PING_PING) 1400 .data(Tags.PING_HEARTBEAT_INTERVAL, 1401 Integer.toString(pingHeartbeat)) 1402 .start(Tags.PING_FOLDERS); 1403 } 1404 1405 String folderClass = getTargetCollectionClassFromCursor(c); 1406 s.start(Tags.PING_FOLDER) 1407 .data(Tags.PING_ID, c.getString(Mailbox.CONTENT_SERVER_ID_COLUMN)) 1408 .data(Tags.PING_CLASS, folderClass) 1409 .end(); 1410 readyMailboxes.add(mailboxName); 1411 } else if ((pingStatus == SyncManager.PING_STATUS_RUNNING) || 1412 (pingStatus == SyncManager.PING_STATUS_WAITING)) { 1413 notReadyMailboxes.add(mailboxName); 1414 } else if (pingStatus == SyncManager.PING_STATUS_UNABLE) { 1415 pushCount--; 1416 userLog(mailboxName, " in error state; ignore"); 1417 continue; 1418 } 1419 } 1420 } finally { 1421 c.close(); 1422 } 1423 1424 if (Eas.USER_LOG) { 1425 if (!notReadyMailboxes.isEmpty()) { 1426 userLog("Ping not ready for: " + notReadyMailboxes); 1427 } 1428 if (!readyMailboxes.isEmpty()) { 1429 userLog("Ping ready for: " + readyMailboxes); 1430 } 1431 } 1432 1433 // If we've waited 10 seconds or more, just ping with whatever boxes are ready 1434 // But use a shorter than normal heartbeat 1435 boolean forcePing = !notReadyMailboxes.isEmpty() && (pingWaitCount > 5); 1436 1437 if ((canPushCount > 0) && ((canPushCount == pushCount) || forcePing)) { 1438 // If all pingable boxes are ready for push, send Ping to the server 1439 s.end().end().done(); 1440 pingWaitCount = 0; 1441 mPostReset = false; 1442 mPostAborted = false; 1443 1444 // If we've been stopped, this is a good time to return 1445 if (mStop) return; 1446 1447 long pingTime = SystemClock.elapsedRealtime(); 1448 try { 1449 // Send the ping, wrapped by appropriate timeout/alarm 1450 if (forcePing) { 1451 userLog("Forcing ping after waiting for all boxes to be ready"); 1452 } 1453 HttpResponse res = 1454 sendPing(s.toByteArray(), forcePing ? PING_FORCE_HEARTBEAT : pingHeartbeat); 1455 1456 int code = res.getStatusLine().getStatusCode(); 1457 userLog("Ping response: ", code); 1458 1459 // Return immediately if we've been asked to stop during the ping 1460 if (mStop) { 1461 userLog("Stopping pingLoop"); 1462 return; 1463 } 1464 1465 if (code == HttpStatus.SC_OK) { 1466 // Make sure to clear out any pending sync errors 1467 SyncManager.removeFromSyncErrorMap(mMailboxId); 1468 HttpEntity e = res.getEntity(); 1469 int len = (int)e.getContentLength(); 1470 InputStream is = res.getEntity().getContent(); 1471 if (len != 0) { 1472 int pingResult = parsePingResult(is, mContentResolver, pingErrorMap); 1473 // If our ping completed (status = 1), and we weren't forced and we're 1474 // not at the maximum, try increasing timeout by two minutes 1475 if (pingResult == PROTOCOL_PING_STATUS_COMPLETED && !forcePing) { 1476 if (pingHeartbeat > mPingHighWaterMark) { 1477 mPingHighWaterMark = pingHeartbeat; 1478 userLog("Setting high water mark at: ", mPingHighWaterMark); 1479 } 1480 if ((pingHeartbeat < PING_MAX_HEARTBEAT) && 1481 !mPingHeartbeatDropped) { 1482 pingHeartbeat += PING_HEARTBEAT_INCREMENT; 1483 if (pingHeartbeat > PING_MAX_HEARTBEAT) { 1484 pingHeartbeat = PING_MAX_HEARTBEAT; 1485 } 1486 userLog("Increasing ping heartbeat to ", pingHeartbeat, "s"); 1487 } 1488 } 1489 } else { 1490 userLog("Ping returned empty result; throwing IOException"); 1491 throw new IOException(); 1492 } 1493 } else if (isAuthError(code)) { 1494 mExitStatus = EXIT_LOGIN_FAILURE; 1495 userLog("Authorization error during Ping: ", code); 1496 throw new IOException(); 1497 } 1498 } catch (IOException e) { 1499 String message = e.getMessage(); 1500 // If we get the exception that is indicative of a NAT timeout and if we 1501 // haven't yet "fixed" the timeout, back off by two minutes and "fix" it 1502 boolean hasMessage = message != null; 1503 userLog("IOException runPingLoop: " + (hasMessage ? message : "[no message]")); 1504 if (mPostReset) { 1505 // Nothing to do in this case; this is SyncManager telling us to try another 1506 // ping. 1507 } else if (mPostAborted || (hasMessage && message.contains("reset by peer"))) { 1508 long pingLength = SystemClock.elapsedRealtime() - pingTime; 1509 if ((pingHeartbeat > PING_MIN_HEARTBEAT) && 1510 (pingHeartbeat > mPingHighWaterMark)) { 1511 pingHeartbeat -= PING_HEARTBEAT_INCREMENT; 1512 mPingHeartbeatDropped = true; 1513 if (pingHeartbeat < PING_MIN_HEARTBEAT) { 1514 pingHeartbeat = PING_MIN_HEARTBEAT; 1515 } 1516 userLog("Decreased ping heartbeat to ", pingHeartbeat, "s"); 1517 } else if (mPostAborted) { 1518 // There's no point in throwing here; this can happen in two cases 1519 // 1) An alarm, which indicates minutes without activity; no sense 1520 // backing off 1521 // 2) SyncManager abort, due to sync of mailbox. Again, we want to 1522 // keep on trying to ping 1523 userLog("Ping aborted; retry"); 1524 } else if (pingLength < 2000) { 1525 userLog("Abort or NAT type return < 2 seconds; throwing IOException"); 1526 throw e; 1527 } else { 1528 userLog("NAT type IOException > 2 seconds?"); 1529 } 1530 } else { 1531 throw e; 1532 } 1533 } 1534 } else if (forcePing) { 1535 // In this case, there aren't any boxes that are pingable, but there are boxes 1536 // waiting (for IOExceptions) 1537 userLog("pingLoop waiting 60s for any pingable boxes"); 1538 sleep(60*SECONDS, true); 1539 } else if (pushCount > 0) { 1540 // If we want to Ping, but can't just yet, wait a little bit 1541 // TODO Change sleep to wait and use notify from SyncManager when a sync ends 1542 sleep(2*SECONDS, false); 1543 pingWaitCount++; 1544 //userLog("pingLoop waited 2s for: ", (pushCount - canPushCount), " box(es)"); 1545 } else if (uninitCount > 0) { 1546 // In this case, we're doing an initial sync of at least one mailbox. Since this 1547 // is typically a one-time case, I'm ok with trying again every 10 seconds until 1548 // we're in one of the other possible states. 1549 userLog("pingLoop waiting for initial sync of ", uninitCount, " box(es)"); 1550 sleep(10*SECONDS, true); 1551 } else { 1552 // We've got nothing to do, so we'll check again in 30 minutes at which time 1553 // we'll update the folder list. Let the device sleep in the meantime... 1554 userLog("pingLoop sleeping for 30m"); 1555 sleep(30*MINUTES, true); 1556 } 1557 } 1558 1559 // Save away the current heartbeat 1560 mPingHeartbeat = pingHeartbeat; 1561 } 1562 1563 void sleep(long ms, boolean runAsleep) { 1564 if (runAsleep) { 1565 SyncManager.runAsleep(mMailboxId, ms+(5*SECONDS)); 1566 } 1567 try { 1568 Thread.sleep(ms); 1569 } catch (InterruptedException e) { 1570 // Doesn't matter whether we stop early; it's the thought that counts 1571 } finally { 1572 if (runAsleep) { 1573 SyncManager.runAwake(mMailboxId); 1574 } 1575 } 1576 } 1577 1578 private int parsePingResult(InputStream is, ContentResolver cr, 1579 HashMap<String, Integer> errorMap) 1580 throws IOException, StaleFolderListException { 1581 PingParser pp = new PingParser(is, this); 1582 if (pp.parse()) { 1583 // True indicates some mailboxes need syncing... 1584 // syncList has the serverId's of the mailboxes... 1585 mBindArguments[0] = Long.toString(mAccount.mId); 1586 mPingChangeList = pp.getSyncList(); 1587 for (String serverId: mPingChangeList) { 1588 mBindArguments[1] = serverId; 1589 Cursor c = cr.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, 1590 WHERE_ACCOUNT_KEY_AND_SERVER_ID, mBindArguments, null); 1591 try { 1592 if (c.moveToFirst()) { 1593 1594 /** 1595 * Check the boxes reporting changes to see if there really were any... 1596 * We do this because bugs in various Exchange servers can put us into a 1597 * looping behavior by continually reporting changes in a mailbox, even when 1598 * there aren't any. 1599 * 1600 * This behavior is seemingly random, and therefore we must code defensively 1601 * by backing off of push behavior when it is detected. 1602 * 1603 * One known cause, on certain Exchange 2003 servers, is acknowledged by 1604 * Microsoft, and the server hotfix for this case can be found at 1605 * http://support.microsoft.com/kb/923282 1606 */ 1607 1608 // Check the status of the last sync 1609 String status = c.getString(Mailbox.CONTENT_SYNC_STATUS_COLUMN); 1610 int type = SyncManager.getStatusType(status); 1611 // This check should always be true... 1612 if (type == SyncManager.SYNC_PING) { 1613 int changeCount = SyncManager.getStatusChangeCount(status); 1614 if (changeCount > 0) { 1615 errorMap.remove(serverId); 1616 } else if (changeCount == 0) { 1617 // This means that a ping reported changes in error; we keep a count 1618 // of consecutive errors of this kind 1619 String name = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN); 1620 Integer failures = errorMap.get(serverId); 1621 if (failures == null) { 1622 userLog("Last ping reported changes in error for: ", name); 1623 errorMap.put(serverId, 1); 1624 } else if (failures > MAX_PING_FAILURES) { 1625 // We'll back off of push for this box 1626 pushFallback(c.getLong(Mailbox.CONTENT_ID_COLUMN)); 1627 continue; 1628 } else { 1629 userLog("Last ping reported changes in error for: ", name); 1630 errorMap.put(serverId, failures + 1); 1631 } 1632 } 1633 } 1634 1635 // If there were no problems with previous sync, we'll start another one 1636 SyncManager.startManualSync(c.getLong(Mailbox.CONTENT_ID_COLUMN), 1637 SyncManager.SYNC_PING, null); 1638 } 1639 } finally { 1640 c.close(); 1641 } 1642 } 1643 } 1644 return pp.getSyncStatus(); 1645 } 1646 1647 private String getEmailFilter() { 1648 String filter = Eas.FILTER_1_WEEK; 1649 switch (mAccount.mSyncLookback) { 1650 case com.android.email.Account.SYNC_WINDOW_1_DAY: { 1651 filter = Eas.FILTER_1_DAY; 1652 break; 1653 } 1654 case com.android.email.Account.SYNC_WINDOW_3_DAYS: { 1655 filter = Eas.FILTER_3_DAYS; 1656 break; 1657 } 1658 case com.android.email.Account.SYNC_WINDOW_1_WEEK: { 1659 filter = Eas.FILTER_1_WEEK; 1660 break; 1661 } 1662 case com.android.email.Account.SYNC_WINDOW_2_WEEKS: { 1663 filter = Eas.FILTER_2_WEEKS; 1664 break; 1665 } 1666 case com.android.email.Account.SYNC_WINDOW_1_MONTH: { 1667 filter = Eas.FILTER_1_MONTH; 1668 break; 1669 } 1670 case com.android.email.Account.SYNC_WINDOW_ALL: { 1671 filter = Eas.FILTER_ALL; 1672 break; 1673 } 1674 } 1675 return filter; 1676 } 1677 1678 /** 1679 * Common code to sync E+PIM data 1680 * 1681 * @param target, an EasMailbox, EasContacts, or EasCalendar object 1682 */ 1683 public void sync(AbstractSyncAdapter target) throws IOException { 1684 Mailbox mailbox = target.mMailbox; 1685 1686 boolean moreAvailable = true; 1687 while (!mStop && moreAvailable) { 1688 // If we have no connectivity, just exit cleanly. SyncManager will start us up again 1689 // when connectivity has returned 1690 if (!hasConnectivity()) { 1691 userLog("No connectivity in sync; finishing sync"); 1692 mExitStatus = EXIT_DONE; 1693 return; 1694 } 1695 1696 // Every time through the loop we check to see if we're still syncable 1697 if (!target.isSyncable()) { 1698 mExitStatus = EXIT_DONE; 1699 return; 1700 } 1701 1702 // Now, handle various requests 1703 while (true) { 1704 Request req = null; 1705 synchronized (mRequests) { 1706 if (mRequests.isEmpty()) { 1707 break; 1708 } else { 1709 req = mRequests.get(0); 1710 } 1711 } 1712 1713 // Our two request types are PartRequest (loading attachment) and 1714 // MeetingResponseRequest (respond to a meeting request) 1715 if (req instanceof PartRequest) { 1716 getAttachment((PartRequest)req); 1717 } else if (req instanceof MeetingResponseRequest) { 1718 sendMeetingResponse((MeetingResponseRequest)req); 1719 } 1720 1721 // If there's an exception handling the request, we'll throw it 1722 // Otherwise, we remove the request 1723 synchronized(mRequests) { 1724 mRequests.remove(req); 1725 } 1726 } 1727 1728 Serializer s = new Serializer(); 1729 String className = target.getCollectionName(); 1730 String syncKey = target.getSyncKey(); 1731 userLog("sync, sending ", className, " syncKey: ", syncKey); 1732 s.start(Tags.SYNC_SYNC) 1733 .start(Tags.SYNC_COLLECTIONS) 1734 .start(Tags.SYNC_COLLECTION) 1735 .data(Tags.SYNC_CLASS, className) 1736 .data(Tags.SYNC_SYNC_KEY, syncKey) 1737 .data(Tags.SYNC_COLLECTION_ID, mailbox.mServerId) 1738 .tag(Tags.SYNC_DELETES_AS_MOVES); 1739 1740 // EAS doesn't like GetChanges if the syncKey is "0"; not documented 1741 if (!syncKey.equals("0")) { 1742 s.tag(Tags.SYNC_GET_CHANGES); 1743 } 1744 s.data(Tags.SYNC_WINDOW_SIZE, 1745 className.equals("Email") ? EMAIL_WINDOW_SIZE : PIM_WINDOW_SIZE); 1746 1747 // Handle options 1748 s.start(Tags.SYNC_OPTIONS); 1749 // Set the lookback appropriately (EAS calls this a "filter") for all but Contacts 1750 if (className.equals("Email")) { 1751 s.data(Tags.SYNC_FILTER_TYPE, getEmailFilter()); 1752 } else if (className.equals("Calendar")) { 1753 // TODO Force one month for calendar until we can set this! 1754 s.data(Tags.SYNC_FILTER_TYPE, Eas.FILTER_1_MONTH); 1755 } 1756 // Set the truncation amount for all classes 1757 if (mProtocolVersionDouble >= 12.0) { 1758 s.start(Tags.BASE_BODY_PREFERENCE) 1759 // HTML for email; plain text for everything else 1760 .data(Tags.BASE_TYPE, (className.equals("Email") ? Eas.BODY_PREFERENCE_HTML 1761 : Eas.BODY_PREFERENCE_TEXT)) 1762 .data(Tags.BASE_TRUNCATION_SIZE, Eas.EAS12_TRUNCATION_SIZE) 1763 .end(); 1764 } else { 1765 s.data(Tags.SYNC_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE); 1766 } 1767 s.end(); 1768 1769 // Send our changes up to the server 1770 target.sendLocalChanges(s); 1771 1772 s.end().end().end().done(); 1773 HttpResponse resp = sendHttpClientPost("Sync", s.toByteArray()); 1774 int code = resp.getStatusLine().getStatusCode(); 1775 if (code == HttpStatus.SC_OK) { 1776 InputStream is = resp.getEntity().getContent(); 1777 if (is != null) { 1778 moreAvailable = target.parse(is); 1779 target.cleanup(); 1780 } else { 1781 userLog("Empty input stream in sync command response"); 1782 } 1783 } else { 1784 userLog("Sync response error: ", code); 1785 if (isProvisionError(code)) { 1786 if (!tryProvision()) { 1787 mExitStatus = EXIT_SECURITY_FAILURE; 1788 return; 1789 } 1790 } else if (isAuthError(code)) { 1791 mExitStatus = EXIT_LOGIN_FAILURE; 1792 } else { 1793 mExitStatus = EXIT_IO_ERROR; 1794 } 1795 return; 1796 } 1797 } 1798 mExitStatus = EXIT_DONE; 1799 } 1800 1801 protected boolean setupService() { 1802 // Make sure account and mailbox are always the latest from the database 1803 mAccount = Account.restoreAccountWithId(mContext, mAccount.mId); 1804 if (mAccount == null) return false; 1805 mMailbox = Mailbox.restoreMailboxWithId(mContext, mMailbox.mId); 1806 if (mMailbox == null) return false; 1807 mThread = Thread.currentThread(); 1808 android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND); 1809 TAG = mThread.getName(); 1810 1811 HostAuth ha = HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv); 1812 if (ha == null) return false; 1813 mHostAddress = ha.mAddress; 1814 mUserName = ha.mLogin; 1815 mPassword = ha.mPassword; 1816 1817 // Set up our protocol version from the Account 1818 mProtocolVersion = mAccount.mProtocolVersion; 1819 // If it hasn't been set up, start with default version 1820 if (mProtocolVersion == null) { 1821 mProtocolVersion = DEFAULT_PROTOCOL_VERSION; 1822 } 1823 mProtocolVersionDouble = Double.parseDouble(mProtocolVersion); 1824 return true; 1825 } 1826 1827 /* (non-Javadoc) 1828 * @see java.lang.Runnable#run() 1829 */ 1830 public void run() { 1831 if (!setupService()) return; 1832 1833 try { 1834 SyncManager.callback().syncMailboxStatus(mMailboxId, EmailServiceStatus.IN_PROGRESS, 0); 1835 } catch (RemoteException e1) { 1836 // Don't care if this fails 1837 } 1838 1839 // Whether or not we're the account mailbox 1840 try { 1841 mDeviceId = SyncManager.getDeviceId(); 1842 if ((mMailbox == null) || (mAccount == null)) { 1843 return; 1844 } else if (mMailbox.mType == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) { 1845 runAccountMailbox(); 1846 } else { 1847 AbstractSyncAdapter target; 1848 if (mMailbox.mType == Mailbox.TYPE_CONTACTS) { 1849 target = new ContactsSyncAdapter(mMailbox, this); 1850 } else if (mMailbox.mType == Mailbox.TYPE_CALENDAR) { 1851 target = new CalendarSyncAdapter(mMailbox, this); 1852 } else { 1853 target = new EmailSyncAdapter(mMailbox, this); 1854 } 1855 // We loop here because someone might have put a request in while we were syncing 1856 // and we've missed that opportunity... 1857 do { 1858 if (mRequestTime != 0) { 1859 userLog("Looping for user request..."); 1860 mRequestTime = 0; 1861 } 1862 sync(target); 1863 } while (mRequestTime != 0); 1864 } 1865 } catch (EasAuthenticationException e) { 1866 userLog("Caught authentication error"); 1867 mExitStatus = EXIT_LOGIN_FAILURE; 1868 } catch (IOException e) { 1869 String message = e.getMessage(); 1870 userLog("Caught IOException: ", (message == null) ? "No message" : message); 1871 mExitStatus = EXIT_IO_ERROR; 1872 } catch (Exception e) { 1873 userLog("Uncaught exception in EasSyncService", e); 1874 } finally { 1875 int status; 1876 1877 if (!mStop) { 1878 userLog("Sync finished"); 1879 SyncManager.done(this); 1880 switch (mExitStatus) { 1881 case EXIT_IO_ERROR: 1882 status = EmailServiceStatus.CONNECTION_ERROR; 1883 break; 1884 case EXIT_DONE: 1885 status = EmailServiceStatus.SUCCESS; 1886 ContentValues cv = new ContentValues(); 1887 cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis()); 1888 String s = "S" + mSyncReason + ':' + status + ':' + mChangeCount; 1889 cv.put(Mailbox.SYNC_STATUS, s); 1890 mContentResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, 1891 mMailboxId), cv, null, null); 1892 break; 1893 case EXIT_LOGIN_FAILURE: 1894 status = EmailServiceStatus.LOGIN_FAILED; 1895 break; 1896 case EXIT_SECURITY_FAILURE: 1897 status = EmailServiceStatus.SECURITY_FAILURE; 1898 break; 1899 default: 1900 status = EmailServiceStatus.REMOTE_EXCEPTION; 1901 errorLog("Sync ended due to an exception."); 1902 break; 1903 } 1904 } else { 1905 userLog("Stopped sync finished."); 1906 status = EmailServiceStatus.SUCCESS; 1907 } 1908 1909 try { 1910 SyncManager.callback().syncMailboxStatus(mMailboxId, status, 0); 1911 } catch (RemoteException e1) { 1912 // Don't care if this fails 1913 } 1914 1915 // Make sure SyncManager knows about this 1916 SyncManager.kick("sync finished"); 1917 } 1918 } 1919} 1920