1 /* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.exchange.eas; 18 19 import android.content.ContentResolver; 20 import android.content.ContentUris; 21 import android.content.ContentValues; 22 import android.content.Context; 23 import android.content.SyncResult; 24 import android.net.Uri; 25 import android.os.Build; 26 import android.os.Bundle; 27 import android.telephony.TelephonyManager; 28 import android.text.TextUtils; 29 import android.text.format.DateUtils; 30 31 import com.android.emailcommon.provider.Account; 32 import com.android.emailcommon.provider.EmailContent; 33 import com.android.emailcommon.provider.HostAuth; 34 import com.android.emailcommon.provider.Mailbox; 35 import com.android.emailcommon.utility.Utility; 36 import com.android.exchange.CommandStatusException; 37 import com.android.exchange.Eas; 38 import com.android.exchange.EasResponse; 39 import com.android.exchange.adapter.Serializer; 40 import com.android.exchange.adapter.Tags; 41 import com.android.exchange.service.EasServerConnection; 42 import com.android.mail.providers.UIProvider; 43 import com.android.mail.utils.LogUtils; 44 45 import org.apache.http.HttpEntity; 46 import org.apache.http.client.methods.HttpUriRequest; 47 import org.apache.http.entity.ByteArrayEntity; 48 49 import java.io.IOException; 50 import java.security.cert.CertificateException; 51 import java.util.ArrayList; 52 53 /** 54 * Base class for all Exchange operations that use a POST to talk to the server. 55 * 56 * The core of this class is {@link #performOperation}, which provides the skeleton of making 57 * a request, handling common errors, and setting fields on the {@link SyncResult} if there is one. 58 * This class abstracts the connection handling from its subclasses and callers. 59 * 60 * {@link #performOperation} calls various abstract functions to create the request and parse the 61 * response. For the most part subclasses can implement just these bits of functionality and rely 62 * on {@link #performOperation} to do all the boilerplate etc. 63 * 64 * There are also a set of functions that a subclass may override if it's substantially 65 * different from the "normal" operation (e.g. autodiscover deviates from the standard URI since 66 * it's not account-specific so it needs to override {@link #getRequestUri()}), but the default 67 * implementations of these functions should suffice for most operations. 68 * 69 * Some subclasses may need to override {@link #performOperation} to add validation and results 70 * processing around a call to super.performOperation. Subclasses should avoid doing too much more 71 * than wrapping some handling around the chained call; if you find that's happening, it's likely 72 * a sign that the base class needs to be enhanced. 73 * 74 * One notable reason this wrapping happens is for operations that need to return a result directly 75 * to their callers (as opposed to simply writing the results to the provider, as is common with 76 * sync operations). This happens for example in 77 * {@link com.android.emailcommon.service.IEmailService} message handlers. In such cases, due to 78 * how {@link com.android.exchange.service.EasService} uses this class, the subclass needs to 79 * store the result as a member variable and then provide an accessor to read the result. Since 80 * different operations have different results (or none at all), there is no function in the base 81 * class for this. 82 * 83 * Note that it is not practical to avoid the race between when an operation loads its account data 84 * and when it uses it, as that would require some form of locking in the provider. There are three 85 * interesting situations where this might happen, and that this class must handle: 86 * 87 * 1) Deleted from provider: Any subsequent provider access should return an error. Operations 88 * must detect this and terminate with an error. 89 * 2) Account sync settings change: Generally only affects Ping. We interrupt the operation and 90 * load the new settings before proceeding. 91 * 3) Sync suspended due to hold: A special case of the previous, and affects all operations, but 92 * fortunately doesn't need special handling here. Correct provider functionality must generate 93 * write failures, so the handling for #1 should cover this case as well. 94 * 95 * This class attempts to defer loading of account data as long as possible -- ideally we load 96 * immediately before the network request -- but does not proactively check for changes after that. 97 * This approach is a a practical balance between minimizing the race without adding too much 98 * complexity beyond what's required. 99 */ 100 public abstract class EasOperation { 101 public static final String LOG_TAG = Eas.LOG_TAG; 102 103 /** The maximum number of server redirects we allow before returning failure. */ 104 private static final int MAX_REDIRECTS = 3; 105 106 /** Message MIME type for EAS version 14 and later. */ 107 private static final String EAS_14_MIME_TYPE = "application/vnd.ms-sync.wbxml"; 108 109 /** 110 * EasOperation error codes below. All subclasses should try to create error codes 111 * that do not overlap these codes or the codes of other subclasses. The error 112 * code values for each subclass should start in a different 100 range (i.e. -100, 113 * -200, etc...). 114 */ 115 116 /** Error code indicating the operation was cancelled via {@link #abort}. */ 117 public static final int RESULT_ABORT = -1; 118 /** Error code indicating the operation was cancelled via {@link #restart}. */ 119 public static final int RESULT_RESTART = -2; 120 /** Error code indicating the Exchange servers redirected too many times. */ 121 public static final int RESULT_TOO_MANY_REDIRECTS = -3; 122 /** Error code indicating the request failed due to a network problem. */ 123 public static final int RESULT_REQUEST_FAILURE = -4; 124 /** Error code indicating a 403 (forbidden) error. */ 125 public static final int RESULT_FORBIDDEN = -5; 126 /** Error code indicating an unresolved provisioning error. */ 127 public static final int RESULT_PROVISIONING_ERROR = -6; 128 /** Error code indicating an authentication problem. */ 129 public static final int RESULT_AUTHENTICATION_ERROR = -7; 130 /** Error code indicating the client is missing a certificate. */ 131 public static final int RESULT_CLIENT_CERTIFICATE_REQUIRED = -8; 132 /** Error code indicating we don't have a protocol version in common with the server. */ 133 public static final int RESULT_PROTOCOL_VERSION_UNSUPPORTED = -9; 134 /** Error code indicating a hard error when initializing the operation. */ 135 public static final int RESULT_INITIALIZATION_FAILURE = -10; 136 /** Error code indicating a hard data layer error. */ 137 public static final int RESULT_HARD_DATA_FAILURE = -11; 138 /** Error code indicating that this operation failed, but we should not abort the sync */ 139 /** TODO: This is currently only used in EasOutboxSync, no other place handles it correctly */ 140 public static final int RESULT_NON_FATAL_ERROR = -12; 141 /** Error code indicating some other failure. */ 142 public static final int RESULT_OTHER_FAILURE = -99; 143 /** Constant to delimit where op specific error codes begin. */ 144 public static final int RESULT_OP_SPECIFIC_ERROR_RESULT = -100; 145 146 protected final Context mContext; 147 148 /** The provider id for the account this operation is on. */ 149 private final long mAccountId; 150 151 /** The cached {@link Account} state; can be null if it hasn't been loaded yet. */ 152 protected Account mAccount; 153 154 /** The connection to use for this operation. This is created when {@link #mAccount} is set. */ 155 private EasServerConnection mConnection; 156 157 public class MessageInvalidException extends Exception { 158 public MessageInvalidException(final String message) { 159 super(message); 160 } 161 } 162 163 /** 164 * Constructor which defers loading of account and connection info. 165 * @param context 166 * @param accountId 167 */ 168 protected EasOperation(final Context context, final long accountId) { 169 mContext = context; 170 mAccountId = accountId; 171 } 172 173 protected EasOperation(final Context context, final Account account, 174 final EasServerConnection connection) { 175 this(context, account.mId); 176 mAccount = account; 177 mConnection = connection; 178 } 179 180 protected EasOperation(final Context context, final Account account, final HostAuth hostAuth) { 181 this(context, account, new EasServerConnection(context, account, hostAuth)); 182 } 183 184 protected EasOperation(final Context context, final Account account) { 185 this(context, account, account.getOrCreateHostAuthRecv(context)); 186 } 187 188 /** 189 * This constructor is for use by operations that are created by other operations, e.g. 190 * {@link EasProvision}. It reuses the account and connection of its parent. 191 * @param parentOperation The {@link EasOperation} that is creating us. 192 */ 193 protected EasOperation(final EasOperation parentOperation) { 194 mContext = parentOperation.mContext; 195 mAccountId = parentOperation.mAccountId; 196 mAccount = parentOperation.mAccount; 197 mConnection = parentOperation.mConnection; 198 } 199 200 /** 201 * Some operations happen before the account exists (e.g. account validation). 202 * These operations cannot use {@link #init}, so instead we make a dummy account and 203 * supply a temporary {@link HostAuth}. 204 * @param hostAuth 205 */ 206 protected final void setDummyAccount(final HostAuth hostAuth) { 207 mAccount = new Account(); 208 mAccount.mEmailAddress = hostAuth.mLogin; 209 mConnection = new EasServerConnection(mContext, mAccount, hostAuth); 210 } 211 212 /** 213 * Loads (or reloads) the {@link Account} for this operation, and sets up our connection to the 214 * server. This can be overridden to add additional functionality, but child implementations 215 * should always call super(). 216 * @param allowReload If false, do not perform a load if we already have an {@link Account} 217 * (i.e. just keep the existing one); otherwise allow replacement of the 218 * account. Note that this can result in a valid Account being replaced with 219 * null if the account no longer exists. 220 * @return Whether we now have a valid {@link Account} object. 221 */ 222 public boolean init(final boolean allowReload) { 223 if (mAccount == null || allowReload) { 224 mAccount = Account.restoreAccountWithId(mContext, getAccountId()); 225 if (mAccount != null) { 226 mConnection = new EasServerConnection(mContext, mAccount, 227 mAccount.getOrCreateHostAuthRecv(mContext)); 228 } 229 } 230 return (mAccount != null); 231 } 232 233 public final long getAccountId() { 234 return mAccountId; 235 } 236 237 public final Account getAccount() { 238 return mAccount; 239 } 240 241 /** 242 * Request that this operation terminate. Intended for use by the sync service to interrupt 243 * running operations, primarily Ping. 244 */ 245 public final void abort() { 246 mConnection.stop(EasServerConnection.STOPPED_REASON_ABORT); 247 } 248 249 /** 250 * Request that this operation restart. Intended for use by the sync service to interrupt 251 * running operations, primarily Ping. 252 */ 253 public final void restart() { 254 mConnection.stop(EasServerConnection.STOPPED_REASON_RESTART); 255 } 256 257 /** 258 * The skeleton of performing an operation. This function handles all the common code and 259 * error handling, calling into virtual functions that are implemented or overridden by the 260 * subclass to do the operation-specific logic. 261 * 262 * The result codes work as follows: 263 * - Negative values indicate common error codes and are defined above (the various RESULT_* 264 * constants). 265 * - Non-negative values indicate the result of {@link #handleResponse}. These are obviously 266 * specific to the subclass, and may indicate success or error conditions. 267 * 268 * The common error codes primarily indicate conditions that occur when performing the POST 269 * itself, such as network errors and handling of the HTTP response. However, some errors that 270 * can be indicated in the HTTP response code can also be indicated in the payload of the 271 * response as well, so {@link #handleResponse} should in those cases return the appropriate 272 * negative result code, which will be handled the same as if it had been indicated in the HTTP 273 * response code. 274 * 275 * @return A result code for the outcome of this operation, as described above. 276 */ 277 public int performOperation() { 278 // Make sure the account is loaded if it hasn't already been. 279 if (!init(false)) { 280 LogUtils.i(LOG_TAG, "Failed to initialize %d before sending request for operation %s", 281 getAccountId(), getCommand()); 282 return RESULT_INITIALIZATION_FAILURE; 283 } 284 285 // We handle server redirects by looping, but we need to protect against too much looping. 286 int redirectCount = 0; 287 288 do { 289 // Perform the HTTP request and handle exceptions. 290 final EasResponse response; 291 try { 292 try { 293 response = mConnection.executeHttpUriRequest(makeRequest(), getTimeout()); 294 } finally { 295 onRequestMade(); 296 } 297 } catch (final IOException e) { 298 // If we were stopped, return the appropriate result code. 299 switch (mConnection.getStoppedReason()) { 300 case EasServerConnection.STOPPED_REASON_ABORT: 301 return RESULT_ABORT; 302 case EasServerConnection.STOPPED_REASON_RESTART: 303 return RESULT_RESTART; 304 default: 305 break; 306 } 307 // If we're here, then we had a IOException that's not from a stop request. 308 String message = e.getMessage(); 309 if (message == null) { 310 message = "(no message)"; 311 } 312 LogUtils.i(LOG_TAG, "IOException while sending request: %s", message); 313 return RESULT_REQUEST_FAILURE; 314 } catch (final CertificateException e) { 315 LogUtils.i(LOG_TAG, "CertificateException while sending request: %s", 316 e.getMessage()); 317 return RESULT_CLIENT_CERTIFICATE_REQUIRED; 318 } catch (final MessageInvalidException e) { 319 LogUtils.d(LOG_TAG, "Exception sending request %s", e.getMessage()); 320 return RESULT_NON_FATAL_ERROR; 321 } catch (final IllegalStateException e) { 322 // Subclasses use ISE to signal a hard error when building the request. 323 // TODO: Switch away from ISEs. 324 LogUtils.e(LOG_TAG, e, "Exception while sending request"); 325 return RESULT_HARD_DATA_FAILURE; 326 } 327 328 // The POST completed, so process the response. 329 try { 330 final int result; 331 // First off, the success case. 332 if (response.isSuccess()) { 333 int responseResult; 334 try { 335 responseResult = handleResponse(response); 336 } catch (final IOException e) { 337 LogUtils.e(LOG_TAG, e, "Exception while handling response"); 338 return RESULT_REQUEST_FAILURE; 339 } catch (final CommandStatusException e) { 340 // For some operations (notably Sync & FolderSync), errors are signaled in 341 // the payload of the response. These will have a HTTP 200 response, and the 342 // error condition is only detected during response parsing. 343 // The various parsers handle this by throwing a CommandStatusException. 344 // TODO: Consider having the parsers return the errors instead of throwing. 345 final int status = e.mStatus; 346 LogUtils.e(LOG_TAG, "CommandStatusException: %s, %d", getCommand(), status); 347 if (CommandStatusException.CommandStatus.isNeedsProvisioning(status)) { 348 responseResult = RESULT_PROVISIONING_ERROR; 349 } else if (CommandStatusException.CommandStatus.isDeniedAccess(status)) { 350 responseResult = RESULT_FORBIDDEN; 351 } else { 352 responseResult = RESULT_OTHER_FAILURE; 353 } 354 } 355 result = responseResult; 356 } else { 357 result = handleHttpError(response.getStatus()); 358 } 359 360 // Non-negative results indicate success. Return immediately and bypass the error 361 // handling. 362 if (result >= 0) { 363 return result; 364 } 365 366 // If this operation has distinct handling for 403 errors, do that. 367 if (result == RESULT_FORBIDDEN || (response.isForbidden() && handleForbidden())) { 368 LogUtils.e(LOG_TAG, "Forbidden response"); 369 return RESULT_FORBIDDEN; 370 } 371 372 // Handle provisioning errors. 373 if (result == RESULT_PROVISIONING_ERROR || response.isProvisionError()) { 374 if (handleProvisionError()) { 375 // The provisioning error has been taken care of, so we should re-do this 376 // request. 377 LogUtils.d(LOG_TAG, "Provisioning error handled during %s, retrying", 378 getCommand()); 379 continue; 380 } 381 return RESULT_PROVISIONING_ERROR; 382 } 383 384 // Handle authentication errors. 385 if (response.isAuthError()) { 386 LogUtils.e(LOG_TAG, "Authentication error"); 387 if (response.isMissingCertificate()) { 388 return RESULT_CLIENT_CERTIFICATE_REQUIRED; 389 } 390 return RESULT_AUTHENTICATION_ERROR; 391 } 392 393 // Handle redirects. 394 if (response.isRedirectError()) { 395 ++redirectCount; 396 mConnection.redirectHostAuth(response.getRedirectAddress()); 397 // Note that unlike other errors, we do NOT return here; we just keep looping. 398 } else { 399 // All other errors. 400 LogUtils.e(LOG_TAG, "Generic error for operation %s: status %d, result %d", 401 getCommand(), response.getStatus(), result); 402 // TODO: This probably should return result. 403 return RESULT_OTHER_FAILURE; 404 } 405 } finally { 406 response.close(); 407 } 408 } while (redirectCount < MAX_REDIRECTS); 409 410 // Non-redirects return immediately after handling, so the only way to reach here is if we 411 // looped too many times. 412 LogUtils.e(LOG_TAG, "Too many redirects"); 413 return RESULT_TOO_MANY_REDIRECTS; 414 } 415 416 protected void onRequestMade() { 417 // This can be overridden to do any cleanup that must happen after the request has 418 // been sent. It will always be called, regardless of the status of the request. 419 } 420 421 protected int handleHttpError(final int httpStatus) { 422 // This function can be overriden if the child class needs to change the result code 423 // based on the http response status. 424 return RESULT_OTHER_FAILURE; 425 } 426 427 /** 428 * Reset the protocol version to use for this connection. If it's changed, and our account is 429 * persisted, also write back the changes to the DB. 430 * @param protocolVersion The new protocol version to use, as a string. 431 */ 432 protected final void setProtocolVersion(final String protocolVersion) { 433 final long accountId = getAccountId(); 434 if (mConnection.setProtocolVersion(protocolVersion) && accountId != Account.NOT_SAVED) { 435 final Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId); 436 final ContentValues cv = new ContentValues(2); 437 if (getProtocolVersion() >= 12.0) { 438 final int oldFlags = Utility.getFirstRowInt(mContext, uri, 439 Account.ACCOUNT_FLAGS_PROJECTION, null, null, null, 440 Account.ACCOUNT_FLAGS_COLUMN_FLAGS, 0); 441 final int newFlags = oldFlags 442 | Account.FLAGS_SUPPORTS_GLOBAL_SEARCH + Account.FLAGS_SUPPORTS_SEARCH; 443 if (oldFlags != newFlags) { 444 cv.put(EmailContent.AccountColumns.FLAGS, newFlags); 445 } 446 } 447 cv.put(EmailContent.AccountColumns.PROTOCOL_VERSION, protocolVersion); 448 mContext.getContentResolver().update(uri, cv, null, null); 449 } 450 } 451 452 /** 453 * Create the request object for this operation. 454 * Most operations use a POST, but some use other request types (e.g. Options). 455 * @return An {@link HttpUriRequest}. 456 * @throws IOException 457 */ 458 private final HttpUriRequest makeRequest() throws IOException, MessageInvalidException { 459 final String requestUri = getRequestUri(); 460 if (requestUri == null) { 461 return mConnection.makeOptions(); 462 } 463 return mConnection.makePost(requestUri, getRequestEntity(), 464 getRequestContentType(), addPolicyKeyHeaderToRequest()); 465 } 466 467 /** 468 * The following functions MUST be overridden by subclasses; these are things that are unique 469 * to each operation. 470 */ 471 472 /** 473 * Get the name of the operation, used as the "Cmd=XXX" query param in the request URI. Note 474 * that if you override {@link #getRequestUri}, then this function may be unused for normal 475 * operation, but all subclasses should return something non-null for use with logging. 476 * @return The name of the command for this operation as defined by the EAS protocol, or for 477 * commands that don't need it, a suitable descriptive name for logging. 478 */ 479 protected abstract String getCommand(); 480 481 /** 482 * Build the {@link HttpEntity} which is used to construct the POST. Typically this function 483 * will build the Exchange request using a {@link Serializer} and then call {@link #makeEntity}. 484 * If the subclass is not using a POST, then it should override this to return null. 485 * @return The {@link HttpEntity} to pass to {@link EasServerConnection#makePost}. 486 * @throws IOException 487 */ 488 protected abstract HttpEntity getRequestEntity() throws IOException, MessageInvalidException; 489 490 /** 491 * Parse the response from the Exchange perform whatever actions are dictated by that. 492 * @param response The {@link EasResponse} to our request. 493 * @return A result code. Non-negative values are returned directly to the caller; negative 494 * values 495 * 496 * that is returned to the caller of {@link #performOperation}. 497 * @throws IOException 498 */ 499 protected abstract int handleResponse(final EasResponse response) 500 throws IOException, CommandStatusException; 501 502 /** 503 * The following functions may be overriden by a subclass, but most operations will not need 504 * to do so. 505 */ 506 507 /** 508 * Get the URI for the Exchange server and this operation. Most (signed in) operations need 509 * not override this; the notable operation that needs to override it is auto-discover. 510 * @return 511 */ 512 protected String getRequestUri() { 513 return mConnection.makeUriString(getCommand()); 514 } 515 516 /** 517 * @return Whether to set the X-MS-PolicyKey header. Only Ping does not want this header. 518 */ 519 protected boolean addPolicyKeyHeaderToRequest() { 520 return true; 521 } 522 523 /** 524 * @return The content type of this request. 525 */ 526 protected String getRequestContentType() { 527 return EAS_14_MIME_TYPE; 528 } 529 530 /** 531 * @return The timeout to use for the POST. 532 */ 533 protected long getTimeout() { 534 return 30 * DateUtils.SECOND_IN_MILLIS; 535 } 536 537 /** 538 * If 403 responses should be handled in a special way, this function should be overridden to 539 * do that. 540 * @return Whether we handle 403 responses; if false, then treat 403 as a provisioning error. 541 */ 542 protected boolean handleForbidden() { 543 return false; 544 } 545 546 /** 547 * Handle a provisioning error. Subclasses may override this to do something different, e.g. 548 * to validate rather than actually do the provisioning. 549 * @return 550 */ 551 protected boolean handleProvisionError() { 552 final EasProvision provisionOperation = new EasProvision(this); 553 return provisionOperation.provision(); 554 } 555 556 /** 557 * Convenience methods for subclasses to use. 558 */ 559 560 /** 561 * Convenience method to make an {@link HttpEntity} from {@link Serializer}. 562 */ 563 protected final HttpEntity makeEntity(final Serializer s) { 564 return new ByteArrayEntity(s.toByteArray()); 565 } 566 567 /** 568 * Check whether we should ask the server what protocol versions it supports and set this 569 * account to use that version. 570 * @return Whether we need a new protocol version from the server. 571 */ 572 protected final boolean shouldGetProtocolVersion() { 573 // TODO: Find conditions under which we should check other than not having one yet. 574 return !mConnection.isProtocolVersionSet(); 575 } 576 577 /** 578 * @return The protocol version to use. 579 */ 580 protected final double getProtocolVersion() { 581 return mConnection.getProtocolVersion(); 582 } 583 584 /** 585 * @return Our useragent. 586 */ 587 protected final String getUserAgent() { 588 return mConnection.getUserAgent(); 589 } 590 591 /** 592 * @return Whether we succeeeded in registering the client cert. 593 */ 594 protected final boolean registerClientCert() { 595 return mConnection.registerClientCert(); 596 } 597 598 /** 599 * Add the device information to the current request. 600 * @param s The {@link Serializer} for our current request. 601 * @param context The {@link Context} for current device. 602 * @param userAgent The user agent string that our connection use. 603 */ 604 protected static void expandedAddDeviceInformationToSerializer(final Serializer s, 605 final Context context, final String userAgent) throws IOException { 606 final String deviceId; 607 final String phoneNumber; 608 final String operator; 609 final TelephonyManager tm = (TelephonyManager)context.getSystemService( 610 Context.TELEPHONY_SERVICE); 611 if (tm != null) { 612 deviceId = tm.getDeviceId(); 613 phoneNumber = tm.getLine1Number(); 614 // TODO: This is not perfect and needs to be improved, for at least two reasons: 615 // 1) SIM cards can override this name. 616 // 2) We don't resend this info to the server when we change networks. 617 final String operatorName = tm.getNetworkOperatorName(); 618 final String operatorNumber = tm.getNetworkOperator(); 619 if (!TextUtils.isEmpty(operatorName) && !TextUtils.isEmpty(operatorNumber)) { 620 operator = operatorName + " (" + operatorNumber + ")"; 621 } else if (!TextUtils.isEmpty(operatorName)) { 622 operator = operatorName; 623 } else { 624 operator = operatorNumber; 625 } 626 } else { 627 deviceId = null; 628 phoneNumber = null; 629 operator = null; 630 } 631 632 // TODO: Right now, we won't send this information unless the device is provisioned again. 633 // Potentially, this means that our phone number could be out of date if the user 634 // switches sims. Is there something we can do to force a reprovision? 635 s.start(Tags.SETTINGS_DEVICE_INFORMATION).start(Tags.SETTINGS_SET); 636 s.data(Tags.SETTINGS_MODEL, Build.MODEL); 637 if (deviceId != null) { 638 s.data(Tags.SETTINGS_IMEI, tm.getDeviceId()); 639 } 640 // Set the device friendly name, if we have one. 641 // TODO: Longer term, this should be done without a provider call. 642 final Bundle deviceName = context.getContentResolver().call( 643 EmailContent.CONTENT_URI, EmailContent.DEVICE_FRIENDLY_NAME, null, null); 644 if (deviceName != null) { 645 final String friendlyName = deviceName.getString(EmailContent.DEVICE_FRIENDLY_NAME); 646 if (!TextUtils.isEmpty(friendlyName)) { 647 s.data(Tags.SETTINGS_FRIENDLY_NAME, friendlyName); 648 } 649 } 650 s.data(Tags.SETTINGS_OS, "Android " + Build.VERSION.RELEASE); 651 if (phoneNumber != null) { 652 s.data(Tags.SETTINGS_PHONE_NUMBER, phoneNumber); 653 } 654 // TODO: Consider setting this, but make sure we know what it's used for. 655 // If the user changes the device's locale and we don't do a reprovision, the server's 656 // idea of the language will be wrong. Since we're not sure what this is used for, 657 // right now we're leaving it out. 658 //s.data(Tags.SETTINGS_OS_LANGUAGE, Locale.getDefault().getDisplayLanguage()); 659 s.data(Tags.SETTINGS_USER_AGENT, userAgent); 660 if (operator != null) { 661 s.data(Tags.SETTINGS_MOBILE_OPERATOR, operator); 662 } 663 s.end().end(); // SETTINGS_SET, SETTINGS_DEVICE_INFORMATION 664 } 665 666 /** 667 * Add the device information to the current request. 668 * @param s The {@link Serializer} that contains the payload for this request. 669 */ 670 protected final void addDeviceInformationToSerializer(final Serializer s) 671 throws IOException { 672 final String userAgent = getUserAgent(); 673 expandedAddDeviceInformationToSerializer(s, mContext, userAgent); 674 } 675 676 /** 677 * Convenience method for adding a Message to an account's outbox 678 * @param account The {@link Account} from which to send the message. 679 * @param msg the message to send 680 */ 681 protected final void sendMessage(final Account account, final EmailContent.Message msg) { 682 long mailboxId = Mailbox.findMailboxOfType(mContext, account.mId, Mailbox.TYPE_OUTBOX); 683 // TODO: Improve system mailbox handling. 684 if (mailboxId == Mailbox.NO_MAILBOX) { 685 LogUtils.d(LOG_TAG, "No outbox for account %d, creating it", account.mId); 686 final Mailbox outbox = 687 Mailbox.newSystemMailbox(mContext, account.mId, Mailbox.TYPE_OUTBOX); 688 outbox.save(mContext); 689 mailboxId = outbox.mId; 690 } 691 msg.mMailboxKey = mailboxId; 692 msg.mAccountKey = account.mId; 693 msg.save(mContext); 694 requestSyncForMailbox(new android.accounts.Account(account.mEmailAddress, 695 Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), mailboxId); 696 } 697 698 /** 699 * Issue a {@link android.content.ContentResolver#requestSync} for a specific mailbox. 700 * @param amAccount The {@link android.accounts.Account} for the account we're pinging. 701 * @param mailboxId The id of the mailbox that needs to sync. 702 */ 703 protected static void requestSyncForMailbox(final android.accounts.Account amAccount, 704 final long mailboxId) { 705 final Bundle extras = Mailbox.createSyncBundle(mailboxId); 706 ContentResolver.requestSync(amAccount, EmailContent.AUTHORITY, extras); 707 LogUtils.i(LOG_TAG, "requestSync EasOperation requestSyncForMailbox %s, %s", 708 amAccount.toString(), extras.toString()); 709 } 710 711 protected static void requestSyncForMailboxes(final android.accounts.Account amAccount, 712 final ArrayList<Long> mailboxIds) { 713 final Bundle extras = Mailbox.createSyncBundle(mailboxIds); 714 /** 715 * Please note that it is very possible that we are trying to send a request to the 716 * email sync adapter even though email push is turned off (i.e. this account might only 717 * be syncing calendar or contacts). In this situation we need to make sure that 718 * this request is marked as manual as to ensure that the sync manager does not drop it 719 * on the floor. Right now, this function is only called by EasPing, if it is every called 720 * by another caller, then we should reconsider if manual=true is the right thing to do. 721 */ 722 extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); 723 ContentResolver.requestSync(amAccount, EmailContent.AUTHORITY, extras); 724 LogUtils.i(LOG_TAG, "requestSync EasOperation requestSyncForMailboxes %s, %s", 725 amAccount.toString(), extras.toString()); 726 } 727 728 /** 729 * RequestNoOpSync 730 * This requests a sync for a particular authority purely so that that account 731 * in settings will recognize that it is trying to sync, and will display the 732 * appropriate UI. In fact, all exchange data syncing actually happens through the 733 * EmailSyncAdapterService. 734 * @param amAccount 735 * @param authority 736 */ 737 protected static void requestNoOpSync(final android.accounts.Account amAccount, 738 final String authority) { 739 final Bundle extras = new Bundle(1); 740 extras.putBoolean(Mailbox.SYNC_EXTRA_NOOP, true); 741 ContentResolver.requestSync(amAccount, authority, extras); 742 LogUtils.d(LOG_TAG, "requestSync EasOperation requestNoOpSync %s, %s", 743 amAccount.toString(), extras.toString()); 744 } 745 746 /** 747 * Interpret a result code from an {@link EasOperation} and, if it's an error, write it to 748 * the appropriate field in {@link SyncResult}. 749 * @param result 750 * @param syncResult 751 * @return Whether an error code was written to syncResult. 752 */ 753 public static boolean writeResultToSyncResult(final int result, final SyncResult syncResult) { 754 switch (result) { 755 case RESULT_TOO_MANY_REDIRECTS: 756 syncResult.tooManyRetries = true; 757 return true; 758 case RESULT_REQUEST_FAILURE: 759 syncResult.stats.numIoExceptions = 1; 760 return true; 761 case RESULT_FORBIDDEN: 762 case RESULT_PROVISIONING_ERROR: 763 case RESULT_AUTHENTICATION_ERROR: 764 case RESULT_CLIENT_CERTIFICATE_REQUIRED: 765 syncResult.stats.numAuthExceptions = 1; 766 return true; 767 case RESULT_PROTOCOL_VERSION_UNSUPPORTED: 768 // Only used in validate, so there's never a syncResult to write to here. 769 break; 770 case RESULT_INITIALIZATION_FAILURE: 771 case RESULT_HARD_DATA_FAILURE: 772 syncResult.databaseError = true; 773 return true; 774 case RESULT_OTHER_FAILURE: 775 // TODO: Is this correct? 776 syncResult.stats.numIoExceptions = 1; 777 return true; 778 } 779 return false; 780 } 781 782 public static int translateSyncResultToUiResult(final int result) { 783 switch (result) { 784 case RESULT_TOO_MANY_REDIRECTS: 785 return UIProvider.LastSyncResult.INTERNAL_ERROR; 786 case RESULT_REQUEST_FAILURE: 787 return UIProvider.LastSyncResult.CONNECTION_ERROR; 788 case RESULT_FORBIDDEN: 789 case RESULT_PROVISIONING_ERROR: 790 case RESULT_AUTHENTICATION_ERROR: 791 case RESULT_CLIENT_CERTIFICATE_REQUIRED: 792 return UIProvider.LastSyncResult.AUTH_ERROR; 793 case RESULT_PROTOCOL_VERSION_UNSUPPORTED: 794 // Only used in validate, so there's never a syncResult to write to here. 795 break; 796 case RESULT_INITIALIZATION_FAILURE: 797 case RESULT_HARD_DATA_FAILURE: 798 return UIProvider.LastSyncResult.INTERNAL_ERROR; 799 case RESULT_OTHER_FAILURE: 800 return UIProvider.LastSyncResult.INTERNAL_ERROR; 801 } 802 return UIProvider.LastSyncResult.SUCCESS; 803 } 804 } 805