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