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