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