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