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