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.format.DateUtils; 29 30 import com.android.emailcommon.provider.Account; 31 import com.android.emailcommon.provider.EmailContent; 32 import com.android.emailcommon.provider.HostAuth; 33 import com.android.emailcommon.provider.Mailbox; 34 import com.android.emailcommon.utility.Utility; 35 import com.android.exchange.Eas; 36 import com.android.exchange.EasResponse; 37 import com.android.exchange.adapter.Serializer; 38 import com.android.exchange.adapter.Tags; 39 import com.android.exchange.service.EasServerConnection; 40 import com.android.mail.utils.LogUtils; 41 42 import org.apache.http.HttpEntity; 43 import org.apache.http.client.methods.HttpUriRequest; 44 import org.apache.http.entity.ByteArrayEntity; 45 46 import java.io.IOException; 47 import java.util.ArrayList; 48 49 /** 50 * Base class for all Exchange operations that use a POST to talk to the server. 51 * 52 * The core of this class is {@link #performOperation}, which provides the skeleton of making 53 * a request, handling common errors, and setting fields on the {@link SyncResult} if there is one. 54 * This class abstracts the connection handling from its subclasses and callers. 55 * 56 * A subclass must implement the abstract functions below that create the request and parse the 57 * response. There are also a set of functions that a subclass may override if it's substantially 58 * different from the "normal" operation (e.g. most requests use the same request URI, but auto 59 * discover deviates since it's not account-specific), but the default implementation should suffice 60 * for most. The subclass must also define a public function which calls {@link #performOperation}, 61 * possibly doing nothing other than that. (I chose to force subclasses to do this, rather than 62 * provide that function in the base class, in order to force subclasses to consider, for example, 63 * whether it needs a {@link SyncResult} parameter, and what the proper name for the "doWork" 64 * function ought to be for the subclass.) 65 */ 66 public abstract class EasOperation { 67 public static final String LOG_TAG = Eas.LOG_TAG; 68 69 /** The maximum number of server redirects we allow before returning failure. */ 70 private static final int MAX_REDIRECTS = 3; 71 72 /** Message MIME type for EAS version 14 and later. */ 73 private static final String EAS_14_MIME_TYPE = "application/vnd.ms-sync.wbxml"; 74 75 /** Error code indicating the operation was cancelled via {@link #abort}. */ 76 public static final int RESULT_ABORT = -1; 77 /** Error code indicating the operation was cancelled via {@link #restart}. */ 78 public static final int RESULT_RESTART = -2; 79 /** Error code indicating the Exchange servers redirected too many times. */ 80 public static final int RESULT_TOO_MANY_REDIRECTS = -3; 81 /** Error code indicating the request failed due to a network problem. */ 82 public static final int RESULT_REQUEST_FAILURE = -4; 83 /** Error code indicating a 403 (forbidden) error. */ 84 public static final int RESULT_FORBIDDEN = -5; 85 /** Error code indicating an unresolved provisioning error. */ 86 public static final int RESULT_PROVISIONING_ERROR = -6; 87 /** Error code indicating an authentication problem. */ 88 public static final int RESULT_AUTHENTICATION_ERROR = -7; 89 /** Error code indicating the client is missing a certificate. */ 90 public static final int RESULT_CLIENT_CERTIFICATE_REQUIRED = -8; 91 /** Error code indicating we don't have a protocol version in common with the server. */ 92 public static final int RESULT_PROTOCOL_VERSION_UNSUPPORTED = -9; 93 /** Error code indicating some other failure. */ 94 public static final int RESULT_OTHER_FAILURE = -10; 95 96 protected final Context mContext; 97 98 /** 99 * The account id for this operation. 100 * NOTE: You will be tempted to add a reference to the {@link Account} here. Resist. 101 * It's too easy for that to lead to creep and stale data. 102 */ 103 protected final long mAccountId; 104 private final EasServerConnection mConnection; 105 106 // TODO: Make this private again when EasSyncHandler is converted to be a subclass. 107 protected EasOperation(final Context context, final long accountId, 108 final EasServerConnection connection) { 109 mContext = context; 110 mAccountId = accountId; 111 mConnection = connection; 112 } 113 114 protected EasOperation(final Context context, final Account account, final HostAuth hostAuth) { 115 this(context, account.mId, new EasServerConnection(context, account, hostAuth)); 116 } 117 118 protected EasOperation(final Context context, final Account account) { 119 this(context, account, HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv)); 120 } 121 122 /** 123 * This constructor is for use by operations that are created by other operations, e.g. 124 * {@link EasProvision}. 125 * @param parentOperation The {@link EasOperation} that is creating us. 126 */ 127 protected EasOperation(final EasOperation parentOperation) { 128 this(parentOperation.mContext, parentOperation.mAccountId, parentOperation.mConnection); 129 } 130 131 /** 132 * Request that this operation terminate. Intended for use by the sync service to interrupt 133 * running operations, primarily Ping. 134 */ 135 public final void abort() { 136 mConnection.stop(EasServerConnection.STOPPED_REASON_ABORT); 137 } 138 139 /** 140 * Request that this operation restart. Intended for use by the sync service to interrupt 141 * running operations, primarily Ping. 142 */ 143 public final void restart() { 144 mConnection.stop(EasServerConnection.STOPPED_REASON_RESTART); 145 } 146 147 /** 148 * The skeleton of performing an operation. This function handles all the common code and 149 * error handling, calling into virtual functions that are implemented or overridden by the 150 * subclass to do the operation-specific logic. 151 * 152 * The result codes work as follows: 153 * - Negative values indicate common error codes and are defined above (the various RESULT_* 154 * constants). 155 * - Non-negative values indicate the result of {@link #handleResponse}. These are obviously 156 * specific to the subclass, and may indicate success or error conditions. 157 * 158 * The common error codes primarily indicate conditions that occur when performing the POST 159 * itself, such as network errors and handling of the HTTP response. However, some errors that 160 * can be indicated in the HTTP response code can also be indicated in the payload of the 161 * response as well, so {@link #handleResponse} should in those cases return the appropriate 162 * negative result code, which will be handled the same as if it had been indicated in the HTTP 163 * response code. 164 * 165 * @param syncResult If this operation is a sync, the {@link SyncResult} object that should 166 * be written to for this sync; otherwise null. 167 * @return A result code for the outcome of this operation, as described above. 168 */ 169 protected final int performOperation(final SyncResult syncResult) { 170 // We handle server redirects by looping, but we need to protect against too much looping. 171 int redirectCount = 0; 172 173 do { 174 // Perform the HTTP request and handle exceptions. 175 final EasResponse response; 176 try { 177 if (registerClientCert()) { 178 response = mConnection.executeHttpUriRequest(makeRequest(), getTimeout()); 179 } else { 180 LogUtils.e(LOG_TAG, "Problem registering client cert"); 181 // TODO: Is this the best stat to increment? 182 if (syncResult != null) { 183 ++syncResult.stats.numAuthExceptions; 184 } 185 return RESULT_CLIENT_CERTIFICATE_REQUIRED; 186 } 187 } catch (final IOException e) { 188 // If we were stopped, return the appropriate result code. 189 switch (mConnection.getStoppedReason()) { 190 case EasServerConnection.STOPPED_REASON_ABORT: 191 return RESULT_ABORT; 192 case EasServerConnection.STOPPED_REASON_RESTART: 193 return RESULT_RESTART; 194 default: 195 break; 196 } 197 // If we're here, then we had a IOException that's not from a stop request. 198 String message = e.getMessage(); 199 if (message == null) { 200 message = "(no message)"; 201 } 202 LogUtils.i(LOG_TAG, "IOException while sending request: %s", message); 203 if (syncResult != null) { 204 ++syncResult.stats.numIoExceptions; 205 } 206 return RESULT_REQUEST_FAILURE; 207 } catch (final IllegalStateException e) { 208 // Subclasses use ISE to signal a hard error when building the request. 209 // TODO: Switch away from ISEs. 210 LogUtils.e(LOG_TAG, e, "Exception while sending request"); 211 if (syncResult != null) { 212 syncResult.databaseError = true; 213 } 214 return RESULT_OTHER_FAILURE; 215 } 216 217 // The POST completed, so process the response. 218 try { 219 final int result; 220 // First off, the success case. 221 if (response.isSuccess()) { 222 try { 223 result = handleResponse(response, syncResult); 224 if (result >= 0) { 225 return result; 226 } 227 } catch (final IOException e) { 228 LogUtils.e(LOG_TAG, e, "Exception while handling response"); 229 if (syncResult != null) { 230 ++syncResult.stats.numIoExceptions; 231 } 232 return RESULT_REQUEST_FAILURE; 233 } 234 } else { 235 result = RESULT_OTHER_FAILURE; 236 } 237 238 // If this operation has distinct handling for 403 errors, do that. 239 if (result == RESULT_FORBIDDEN || (response.isForbidden() && handleForbidden())) { 240 LogUtils.e(LOG_TAG, "Forbidden response"); 241 if (syncResult != null) { 242 // TODO: Is this the best stat to increment? 243 ++syncResult.stats.numAuthExceptions; 244 } 245 return RESULT_FORBIDDEN; 246 } 247 248 // Handle provisioning errors. 249 if (result == RESULT_PROVISIONING_ERROR || response.isProvisionError()) { 250 if (handleProvisionError(syncResult, mAccountId)) { 251 // The provisioning error has been taken care of, so we should re-do this 252 // request. 253 continue; 254 } 255 if (syncResult != null) { 256 LogUtils.e(LOG_TAG, "Issue with provisioning"); 257 // TODO: Is this the best stat to increment? 258 ++syncResult.stats.numAuthExceptions; 259 } 260 return RESULT_PROVISIONING_ERROR; 261 } 262 263 // Handle authentication errors. 264 if (response.isAuthError()) { 265 LogUtils.e(LOG_TAG, "Authentication error"); 266 if (syncResult != null) { 267 ++syncResult.stats.numAuthExceptions; 268 } 269 if (response.isMissingCertificate()) { 270 return RESULT_CLIENT_CERTIFICATE_REQUIRED; 271 } 272 return RESULT_AUTHENTICATION_ERROR; 273 } 274 275 // Handle redirects. 276 if (response.isRedirectError()) { 277 ++redirectCount; 278 mConnection.redirectHostAuth(response.getRedirectAddress()); 279 // Note that unlike other errors, we do NOT return here; we just keep looping. 280 } else { 281 // All other errors. 282 LogUtils.e(LOG_TAG, "Generic error for operation %s: status %d, result %d", 283 getCommand(), response.getStatus(), result); 284 if (syncResult != null) { 285 // TODO: Is this the best stat to increment? 286 ++syncResult.stats.numIoExceptions; 287 } 288 return RESULT_OTHER_FAILURE; 289 } 290 } finally { 291 response.close(); 292 } 293 } while (redirectCount < MAX_REDIRECTS); 294 295 // Non-redirects return immediately after handling, so the only way to reach here is if we 296 // looped too many times. 297 LogUtils.e(LOG_TAG, "Too many redirects"); 298 if (syncResult != null) { 299 syncResult.tooManyRetries = true; 300 } 301 return RESULT_TOO_MANY_REDIRECTS; 302 } 303 304 /** 305 * Reset the protocol version to use for this connection. If it's changed, and our account is 306 * persisted, also write back the changes to the DB. 307 * @param protocolVersion The new protocol version to use, as a string. 308 */ 309 protected final void setProtocolVersion(final String protocolVersion) { 310 if (mConnection.setProtocolVersion(protocolVersion) && mAccountId != Account.NOT_SAVED) { 311 final Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, mAccountId); 312 final ContentValues cv = new ContentValues(2); 313 if (getProtocolVersion() >= 12.0) { 314 final int oldFlags = Utility.getFirstRowInt(mContext, uri, 315 Account.ACCOUNT_FLAGS_PROJECTION, null, null, null, 316 Account.ACCOUNT_FLAGS_COLUMN_FLAGS, 0); 317 final int newFlags = oldFlags 318 | Account.FLAGS_SUPPORTS_GLOBAL_SEARCH + Account.FLAGS_SUPPORTS_SEARCH; 319 if (oldFlags != newFlags) { 320 cv.put(EmailContent.AccountColumns.FLAGS, newFlags); 321 } 322 } 323 cv.put(EmailContent.AccountColumns.PROTOCOL_VERSION, protocolVersion); 324 mContext.getContentResolver().update(uri, cv, null, null); 325 } 326 } 327 328 /** 329 * Create the request object for this operation. 330 * Most operations use a POST, but some use other request types (e.g. Options). 331 * @return An {@link HttpUriRequest}. 332 * @throws IOException 333 */ 334 private final HttpUriRequest makeRequest() throws IOException { 335 final String requestUri = getRequestUri(); 336 if (requestUri == null) { 337 return mConnection.makeOptions(); 338 } 339 return mConnection.makePost(requestUri, getRequestEntity(), 340 getRequestContentType(), addPolicyKeyHeaderToRequest()); 341 } 342 343 /** 344 * The following functions MUST be overridden by subclasses; these are things that are unique 345 * to each operation. 346 */ 347 348 /** 349 * Get the name of the operation, used as the "Cmd=XXX" query param in the request URI. Note 350 * that if you override {@link #getRequestUri}, then this function may be unused for normal 351 * operation, but all subclasses should return something non-null for use with logging. 352 * @return The name of the command for this operation as defined by the EAS protocol, or for 353 * commands that don't need it, a suitable descriptive name for logging. 354 */ 355 protected abstract String getCommand(); 356 357 /** 358 * Build the {@link HttpEntity} which is used to construct the POST. Typically this function 359 * will build the Exchange request using a {@link Serializer} and then call {@link #makeEntity}. 360 * If the subclass is not using a POST, then it should override this to return null. 361 * @return The {@link HttpEntity} to pass to {@link EasServerConnection#makePost}. 362 * @throws IOException 363 */ 364 protected abstract HttpEntity getRequestEntity() throws IOException; 365 366 /** 367 * Parse the response from the Exchange perform whatever actions are dictated by that. 368 * @param response The {@link EasResponse} to our request. 369 * @param syncResult The {@link SyncResult} object for this operation, or null if we're not 370 * handling a sync. 371 * @return A result code. Non-negative values are returned directly to the caller; negative 372 * values 373 * 374 * that is returned to the caller of {@link #performOperation}. 375 * @throws IOException 376 */ 377 protected abstract int handleResponse(final EasResponse response, final SyncResult syncResult) 378 throws IOException; 379 380 /** 381 * The following functions may be overriden by a subclass, but most operations will not need 382 * to do so. 383 */ 384 385 /** 386 * Get the URI for the Exchange server and this operation. Most (signed in) operations need 387 * not override this; the notable operation that needs to override it is auto-discover. 388 * @return 389 */ 390 protected String getRequestUri() { 391 return mConnection.makeUriString(getCommand()); 392 } 393 394 /** 395 * @return Whether to set the X-MS-PolicyKey header. Only Ping does not want this header. 396 */ 397 protected boolean addPolicyKeyHeaderToRequest() { 398 return true; 399 } 400 401 /** 402 * @return The content type of this request. 403 */ 404 protected String getRequestContentType() { 405 return EAS_14_MIME_TYPE; 406 } 407 408 /** 409 * @return The timeout to use for the POST. 410 */ 411 protected long getTimeout() { 412 return 30 * DateUtils.SECOND_IN_MILLIS; 413 } 414 415 /** 416 * If 403 responses should be handled in a special way, this function should be overridden to 417 * do that. 418 * @return Whether we handle 403 responses; if false, then treat 403 as a provisioning error. 419 */ 420 protected boolean handleForbidden() { 421 return false; 422 } 423 424 /** 425 * Handle a provisioning error. Subclasses may override this to do something different, e.g. 426 * to validate rather than actually do the provisioning. 427 * @param syncResult 428 * @param accountId 429 * @return 430 */ 431 protected boolean handleProvisionError(final SyncResult syncResult, final long accountId) { 432 final EasProvision provisionOperation = new EasProvision(this); 433 return provisionOperation.provision(syncResult, accountId); 434 } 435 436 /** 437 * Convenience methods for subclasses to use. 438 */ 439 440 /** 441 * Convenience method to make an {@link HttpEntity} from {@link Serializer}. 442 */ 443 protected final HttpEntity makeEntity(final Serializer s) { 444 return new ByteArrayEntity(s.toByteArray()); 445 } 446 447 /** 448 * Check whether we should ask the server what protocol versions it supports and set this 449 * account to use that version. 450 * @return Whether we need a new protocol version from the server. 451 */ 452 protected final boolean shouldGetProtocolVersion() { 453 // TODO: Find conditions under which we should check other than not having one yet. 454 return !mConnection.isProtocolVersionSet(); 455 } 456 457 /** 458 * @return The protocol version to use. 459 */ 460 protected final double getProtocolVersion() { 461 return mConnection.getProtocolVersion(); 462 } 463 464 /** 465 * @return Our useragent. 466 */ 467 protected final String getUserAgent() { 468 return mConnection.getUserAgent(); 469 } 470 471 /** 472 * @return Whether we succeeeded in registering the client cert. 473 */ 474 protected final boolean registerClientCert() { 475 return mConnection.registerClientCert(); 476 } 477 478 /** 479 * Add the device information to the current request. 480 * @param s The {@link Serializer} for our current request. 481 * @throws IOException 482 */ 483 protected final void addDeviceInformationToSerlializer(final Serializer s) throws IOException { 484 final TelephonyManager tm = (TelephonyManager)mContext.getSystemService( 485 Context.TELEPHONY_SERVICE); 486 final String deviceId; 487 final String phoneNumber; 488 final String operator; 489 if (tm != null) { 490 deviceId = tm.getDeviceId(); 491 phoneNumber = tm.getLine1Number(); 492 operator = tm.getNetworkOperator(); 493 } else { 494 deviceId = null; 495 phoneNumber = null; 496 operator = null; 497 } 498 499 // TODO: Right now, we won't send this information unless the device is provisioned again. 500 // Potentially, this means that our phone number could be out of date if the user 501 // switches sims. Is there something we can do to force a reprovision? 502 s.start(Tags.SETTINGS_DEVICE_INFORMATION).start(Tags.SETTINGS_SET); 503 s.data(Tags.SETTINGS_MODEL, Build.MODEL); 504 if (deviceId != null) { 505 s.data(Tags.SETTINGS_IMEI, tm.getDeviceId()); 506 } 507 // TODO: What should we use for friendly name? 508 //s.data(Tags.SETTINGS_FRIENDLY_NAME, "Friendly Name"); 509 s.data(Tags.SETTINGS_OS, "Android " + Build.VERSION.RELEASE); 510 if (phoneNumber != null) { 511 s.data(Tags.SETTINGS_PHONE_NUMBER, phoneNumber); 512 } 513 // TODO: Consider setting this, but make sure we know what it's used for. 514 // If the user changes the device's locale and we don't do a reprovision, the server's 515 // idea of the language will be wrong. Since we're not sure what this is used for, 516 // right now we're leaving it out. 517 //s.data(Tags.SETTINGS_OS_LANGUAGE, Locale.getDefault().getDisplayLanguage()); 518 s.data(Tags.SETTINGS_USER_AGENT, getUserAgent()); 519 if (operator != null) { 520 s.data(Tags.SETTINGS_MOBILE_OPERATOR, operator); 521 } 522 s.end().end(); // SETTINGS_SET, SETTINGS_DEVICE_INFORMATION 523 } 524 525 /** 526 * Convenience method for adding a Message to an account's outbox 527 * @param account The {@link Account} from which to send the message. 528 * @param msg the message to send 529 */ 530 protected final void sendMessage(final Account account, final EmailContent.Message msg) { 531 long mailboxId = Mailbox.findMailboxOfType(mContext, account.mId, Mailbox.TYPE_OUTBOX); 532 // TODO: Improve system mailbox handling. 533 if (mailboxId == Mailbox.NO_MAILBOX) { 534 LogUtils.d(LOG_TAG, "No outbox for account %d, creating it", account.mId); 535 final Mailbox outbox = 536 Mailbox.newSystemMailbox(mContext, account.mId, Mailbox.TYPE_OUTBOX); 537 outbox.save(mContext); 538 mailboxId = outbox.mId; 539 } 540 msg.mMailboxKey = mailboxId; 541 msg.mAccountKey = account.mId; 542 msg.save(mContext); 543 requestSyncForMailbox(new android.accounts.Account(account.mEmailAddress, 544 Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), mailboxId); 545 } 546 547 /** 548 * Issue a {@link android.content.ContentResolver#requestSync} for a specific mailbox. 549 * @param amAccount The {@link android.accounts.Account} for the account we're pinging. 550 * @param mailboxId The id of the mailbox that needs to sync. 551 */ 552 protected static void requestSyncForMailbox(final android.accounts.Account amAccount, 553 final long mailboxId) { 554 final Bundle extras = Mailbox.createSyncBundle(mailboxId); 555 ContentResolver.requestSync(amAccount, EmailContent.AUTHORITY, extras); 556 LogUtils.i(LOG_TAG, "requestSync EasOperation requestSyncForMailbox %s, %s", 557 amAccount.toString(), extras.toString()); 558 } 559 560 protected static void requestSyncForMailboxes(final android.accounts.Account amAccount, 561 final ArrayList<Long> mailboxIds) { 562 final Bundle extras = Mailbox.createSyncBundle(mailboxIds); 563 ContentResolver.requestSync(amAccount, EmailContent.AUTHORITY, extras); 564 LogUtils.i(LOG_TAG, "requestSync EasOperation requestSyncForMailboxes %s, %s", 565 amAccount.toString(), extras.toString()); 566 } 567 568 /** 569 * RequestNoOpSync 570 * This requests a sync for a particular authority purely so that that account 571 * in settings will recognize that it is trying to sync, and will display the 572 * appropriate UI. In fact, all exchange data syncing actually happens through the 573 * EmailSyncAdapterService. 574 * @param amAccount 575 * @param authority 576 */ 577 protected static void requestNoOpSync(final android.accounts.Account amAccount, 578 final String authority) { 579 final Bundle extras = new Bundle(1); 580 extras.putBoolean(Mailbox.SYNC_EXTRA_NOOP, true); 581 ContentResolver.requestSync(amAccount, authority, extras); 582 LogUtils.d(LOG_TAG, "requestSync EasOperation requestNoOpSync %s, %s", 583 amAccount.toString(), extras.toString()); 584 } 585 } 586