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