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