Home | History | Annotate | Download | only in eas
      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