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.ContentValues;
     21 import android.content.Context;
     22 import android.content.SyncResult;
     23 import android.database.Cursor;
     24 import android.os.Bundle;
     25 import android.os.SystemClock;
     26 import android.provider.CalendarContract;
     27 import android.provider.ContactsContract;
     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.EmailContent.AccountColumns;
     33 import com.android.emailcommon.provider.EmailContent.MailboxColumns;
     34 import com.android.emailcommon.provider.Mailbox;
     35 import com.android.exchange.CommandStatusException.CommandStatus;
     36 import com.android.exchange.Eas;
     37 import com.android.exchange.EasResponse;
     38 import com.android.exchange.adapter.PingParser;
     39 import com.android.exchange.adapter.Serializer;
     40 import com.android.exchange.adapter.Tags;
     41 import com.android.mail.utils.LogUtils;
     42 
     43 import org.apache.http.HttpEntity;
     44 
     45 import java.io.IOException;
     46 import java.util.ArrayList;
     47 import java.util.HashMap;
     48 import java.util.HashSet;
     49 import java.util.Set;
     50 
     51 /**
     52  * Performs an Exchange Ping, which is the command for receiving push notifications.
     53  * See http://msdn.microsoft.com/en-us/library/ee200913(v=exchg.80).aspx for more details.
     54  */
     55 public class EasPing extends EasOperation {
     56     private static final String TAG = Eas.LOG_TAG;
     57 
     58     private static final String WHERE_ACCOUNT_KEY_AND_SERVER_ID =
     59             MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SERVER_ID + "=?";
     60 
     61     private final long mAccountId;
     62     private final android.accounts.Account mAmAccount;
     63     private long mPingDuration;
     64 
     65     /**
     66      * The default heartbeat interval specified to the Exchange server. This is the maximum amount
     67      * of time (in seconds) that the server should wait before responding to the ping request.
     68      */
     69     private static final long DEFAULT_PING_HEARTBEAT =
     70             8 * (DateUtils.MINUTE_IN_MILLIS / DateUtils.SECOND_IN_MILLIS);
     71 
     72     /**
     73      * The minimum heartbeat interval we should ever use, in seconds.
     74      */
     75     private static final long MINIMUM_PING_HEARTBEAT =
     76             8 * (DateUtils.MINUTE_IN_MILLIS / DateUtils.SECOND_IN_MILLIS);
     77 
     78     /**
     79      * The maximum heartbeat interval we should ever use, in seconds.
     80      */
     81     private static final long MAXIMUM_PING_HEARTBEAT =
     82             28 * (DateUtils.MINUTE_IN_MILLIS / DateUtils.SECOND_IN_MILLIS);
     83 
     84     /**
     85      * The maximum amount that we can change with each adjustment, in seconds.
     86      */
     87     private static final long MAXIMUM_HEARTBEAT_INCREMENT =
     88             5 * (DateUtils.MINUTE_IN_MILLIS / DateUtils.SECOND_IN_MILLIS);
     89 
     90     /**
     91      * The extra time for the timeout used for the HTTP POST (in milliseconds). Notionally this
     92      * should be the same as ping heartbeat but in practice is a few seconds longer to allow for
     93      * latency in the server's response.
     94      */
     95     private static final long EXTRA_POST_TIMEOUT_MILLIS = 5 * DateUtils.SECOND_IN_MILLIS;
     96 
     97     public EasPing(final Context context, final Account account,
     98             final android.accounts.Account amAccount) {
     99         super(context, account);
    100         mAccountId = account.mId;
    101         mAmAccount = amAccount;
    102         mPingDuration = account.mPingDuration;
    103         if (mPingDuration == 0) {
    104             mPingDuration = DEFAULT_PING_HEARTBEAT;
    105         }
    106         LogUtils.d(TAG, "initial ping duration " + mPingDuration + " account " + mAccountId);
    107     }
    108 
    109     public final int doPing() {
    110         final long startTime = SystemClock.elapsedRealtime();
    111         final int result = performOperation(null);
    112         if (result == RESULT_RESTART) {
    113             return PingParser.STATUS_EXPIRED;
    114         } else  if (result == RESULT_REQUEST_FAILURE) {
    115             final long timeoutDuration = SystemClock.elapsedRealtime() - startTime;
    116             LogUtils.d(TAG, "doPing request failure, timed out after %d millis", timeoutDuration);
    117             decreasePingDuration();
    118         }
    119         return result;
    120     }
    121 
    122     private void decreasePingDuration() {
    123         mPingDuration = Math.max(MINIMUM_PING_HEARTBEAT,
    124                 mPingDuration - MAXIMUM_HEARTBEAT_INCREMENT);
    125         LogUtils.d(TAG, "decreasePingDuration adjusting by " + MAXIMUM_HEARTBEAT_INCREMENT +
    126                 " new duration " + mPingDuration + " account " + mAccountId);
    127         storePingDuration();
    128     }
    129 
    130     private void increasePingDuration() {
    131         mPingDuration = Math.min(MAXIMUM_PING_HEARTBEAT,
    132                 mPingDuration + MAXIMUM_HEARTBEAT_INCREMENT);
    133         LogUtils.d(TAG, "increasePingDuration adjusting by " + MAXIMUM_HEARTBEAT_INCREMENT +
    134                 " new duration " + mPingDuration + " account " + mAccountId);
    135         storePingDuration();
    136     }
    137 
    138     private void storePingDuration() {
    139         final ContentValues values = new ContentValues(1);
    140         values.put(AccountColumns.PING_DURATION, mPingDuration);
    141         Account.update(mContext, Account.CONTENT_URI, mAccountId, values);
    142     }
    143 
    144     public final long getAccountId() {
    145         return mAccountId;
    146     }
    147 
    148     public final android.accounts.Account getAmAccount() {
    149         return mAmAccount;
    150     }
    151 
    152     @Override
    153     protected String getCommand() {
    154         return "Ping";
    155     }
    156 
    157     @Override
    158     protected HttpEntity getRequestEntity() throws IOException {
    159         // Get the mailboxes that need push notifications.
    160         final Cursor c = Mailbox.getMailboxesForPush(mContext.getContentResolver(),
    161                 mAccountId);
    162         if (c == null) {
    163             throw new IllegalStateException("Could not read mailboxes");
    164         }
    165 
    166         // TODO: Ideally we never even get here unless we already know we want a push.
    167         Serializer s = null;
    168         try {
    169             while (c.moveToNext()) {
    170                 final Mailbox mailbox = new Mailbox();
    171                 mailbox.restore(c);
    172                 s = handleOneMailbox(s, mailbox);
    173             }
    174         } finally {
    175             c.close();
    176         }
    177 
    178         if (s == null) {
    179             abort();
    180             throw new IOException("No mailboxes want push");
    181         }
    182         // This sequence of end()s corresponds to the start()s that occur in handleOneMailbox when
    183         // the Serializer is first created. If either side changes, the other must be kept in sync.
    184         s.end().end().done();
    185         return makeEntity(s);
    186     }
    187 
    188     @Override
    189     protected int handleResponse(final EasResponse response, final SyncResult syncResult)
    190             throws IOException {
    191         if (response.isEmpty()) {
    192             // TODO this should probably not be an IOException, maybe something more descriptive?
    193             throw new IOException("Empty ping response");
    194         }
    195 
    196         // Handle a valid response.
    197         final PingParser pp = new PingParser(response.getInputStream());
    198         pp.parse();
    199         final int pingStatus = pp.getPingStatus();
    200 
    201         // Take the appropriate action for this response.
    202         // Many of the responses require no explicit action here, they just influence
    203         // our re-ping behavior, which is handled by the caller.
    204         switch (pingStatus) {
    205             case PingParser.STATUS_EXPIRED:
    206                 LogUtils.i(TAG, "Ping expired for account %d", mAccountId);
    207                 // On successful expiration, we can increase our ping duration
    208                 increasePingDuration();
    209                 break;
    210             case PingParser.STATUS_CHANGES_FOUND:
    211                 LogUtils.i(TAG, "Ping found changed folders for account %d", mAccountId);
    212                 requestSyncForSyncList(pp.getSyncList());
    213                 break;
    214             case PingParser.STATUS_REQUEST_INCOMPLETE:
    215             case PingParser.STATUS_REQUEST_MALFORMED:
    216                 // These two cases indicate that the ping request was somehow bad.
    217                 // TODO: It's insanity to re-ping with the same data and expect a different
    218                 // result. Improve this if possible.
    219                 LogUtils.e(TAG, "Bad ping request for account %d", mAccountId);
    220                 break;
    221             case PingParser.STATUS_REQUEST_HEARTBEAT_OUT_OF_BOUNDS:
    222                 long newDuration = pp.getHeartbeatInterval();
    223                 LogUtils.i(TAG, "Heartbeat out of bounds for account %d, " +
    224                         "old duration %d new duration %d", mAccountId, mPingDuration, newDuration);
    225                 mPingDuration = newDuration;
    226                 storePingDuration();
    227                 break;
    228             case PingParser.STATUS_REQUEST_TOO_MANY_FOLDERS:
    229                 LogUtils.i(TAG, "Too many folders for account %d", mAccountId);
    230                 break;
    231             case PingParser.STATUS_FOLDER_REFRESH_NEEDED:
    232                 LogUtils.i(TAG, "FolderSync needed for account %d", mAccountId);
    233                 requestFolderSync();
    234                 break;
    235             case PingParser.STATUS_SERVER_ERROR:
    236                 LogUtils.i(TAG, "Server error for account %d", mAccountId);
    237                 break;
    238             case CommandStatus.SERVER_ERROR_RETRY:
    239                 // Try again later.
    240                 LogUtils.i(TAG, "Retryable server error for account %d", mAccountId);
    241                 return RESULT_RESTART;
    242 
    243             // These errors should not happen.
    244             case CommandStatus.USER_DISABLED_FOR_SYNC:
    245             case CommandStatus.USERS_DISABLED_FOR_SYNC:
    246             case CommandStatus.USER_ON_LEGACY_SERVER_CANT_SYNC:
    247             case CommandStatus.DEVICE_QUARANTINED:
    248             case CommandStatus.ACCESS_DENIED:
    249             case CommandStatus.USER_ACCOUNT_DISABLED:
    250             case CommandStatus.NOT_PROVISIONABLE_PARTIAL:
    251             case CommandStatus.NOT_PROVISIONABLE_LEGACY_DEVICE:
    252             case CommandStatus.TOO_MANY_PARTNERSHIPS:
    253                 LogUtils.e(TAG, "Unexpected error %d on ping", pingStatus);
    254                 return RESULT_AUTHENTICATION_ERROR;
    255 
    256             // These errors should not happen.
    257             case CommandStatus.SYNC_STATE_NOT_FOUND:
    258             case CommandStatus.SYNC_STATE_LOCKED:
    259             case CommandStatus.SYNC_STATE_CORRUPT:
    260             case CommandStatus.SYNC_STATE_EXISTS:
    261             case CommandStatus.SYNC_STATE_INVALID:
    262             case CommandStatus.NEEDS_PROVISIONING_WIPE:
    263             case CommandStatus.NEEDS_PROVISIONING:
    264             case CommandStatus.NEEDS_PROVISIONING_REFRESH:
    265             case CommandStatus.NEEDS_PROVISIONING_INVALID:
    266             case CommandStatus.WTF_INVALID_COMMAND:
    267             case CommandStatus.WTF_INVALID_PROTOCOL:
    268             case CommandStatus.WTF_DEVICE_CLAIMS_EXTERNAL_MANAGEMENT:
    269             case CommandStatus.WTF_UNKNOWN_ITEM_TYPE:
    270             case CommandStatus.WTF_REQUIRES_PROXY_WITHOUT_SSL:
    271             case CommandStatus.ITEM_NOT_FOUND:
    272                 LogUtils.e(TAG, "Unexpected error %d on ping", pingStatus);
    273                 return RESULT_OTHER_FAILURE;
    274 
    275             default:
    276                 break;
    277         }
    278 
    279         return pingStatus;
    280     }
    281 
    282 
    283     @Override
    284     protected boolean addPolicyKeyHeaderToRequest() {
    285         return false;
    286     }
    287 
    288     @Override
    289     protected long getTimeout() {
    290         return mPingDuration * DateUtils.SECOND_IN_MILLIS + EXTRA_POST_TIMEOUT_MILLIS;
    291     }
    292 
    293     /**
    294      * If mailbox is eligible for push, add it to the ping request, creating the {@link Serializer}
    295      * for the request if necessary.
    296      * @param mailbox The mailbox to check.
    297      * @param s The {@link Serializer} for this request, or null if it hasn't been created yet.
    298      * @return The {@link Serializer} for this request, or null if it hasn't been created yet.
    299      * @throws IOException
    300      */
    301     private Serializer handleOneMailbox(Serializer s, final Mailbox mailbox) throws IOException {
    302         // We can't push until the initial sync is done
    303         if (mailbox.mSyncKey != null && !mailbox.mSyncKey.equals("0")) {
    304             if (ContentResolver.getSyncAutomatically(mAmAccount,
    305                     Mailbox.getAuthority(mailbox.mType))) {
    306                 if (s == null) {
    307                     // No serializer yet, so create and initialize it.
    308                     // Note that these start()s correspond to the end()s in doInBackground.
    309                     // If either side changes, the other must be kept in sync.
    310                     s = new Serializer();
    311                     s.start(Tags.PING_PING);
    312                     s.data(Tags.PING_HEARTBEAT_INTERVAL, Long.toString(mPingDuration));
    313                     s.start(Tags.PING_FOLDERS);
    314                 }
    315                 s.start(Tags.PING_FOLDER);
    316                 s.data(Tags.PING_ID, mailbox.mServerId);
    317                 s.data(Tags.PING_CLASS, Eas.getFolderClass(mailbox.mType));
    318                 s.end();
    319             }
    320         }
    321         return s;
    322     }
    323 
    324     /**
    325      * Make the appropriate calls to {@link ContentResolver#requestSync} indicated by the
    326      * current ping response.
    327      * @param syncList The list of folders that need to be synced.
    328      */
    329     private void requestSyncForSyncList(final ArrayList<String> syncList) {
    330         final String[] bindArguments = new String[2];
    331         bindArguments[0] = Long.toString(mAccountId);
    332 
    333         final ArrayList<Long> mailboxIds = new ArrayList<Long>();
    334         final HashSet<Integer> contentTypes = new HashSet<Integer>();
    335 
    336         for (final String serverId : syncList) {
    337             bindArguments[1] = serverId;
    338             // TODO: Rather than one query per ping mailbox, do it all in one?
    339             final Cursor c = mContext.getContentResolver().query(Mailbox.CONTENT_URI,
    340                     Mailbox.CONTENT_PROJECTION, WHERE_ACCOUNT_KEY_AND_SERVER_ID,
    341                     bindArguments, null);
    342             if (c == null) {
    343                 // TODO: proper error handling.
    344                 break;
    345             }
    346             try {
    347                 /**
    348                  * Check the boxes reporting changes to see if there really were any...
    349                  * We do this because bugs in various Exchange servers can put us into a
    350                  * looping behavior by continually reporting changes in a mailbox, even
    351                  * when there aren't any.
    352                  *
    353                  * This behavior is seemingly random, and therefore we must code
    354                  * defensively by backing off of push behavior when it is detected.
    355                  *
    356                  * One known cause, on certain Exchange 2003 servers, is acknowledged by
    357                  * Microsoft, and the server hotfix for this case can be found at
    358                  * http://support.microsoft.com/kb/923282
    359                  */
    360                 // TODO: Implement the above.
    361                     /*
    362                     String status = c.getString(Mailbox.CONTENT_SYNC_STATUS_COLUMN);
    363                     int type = ExchangeService.getStatusType(status);
    364                     // This check should always be true...
    365                     if (type == ExchangeService.SYNC_PING) {
    366                         int changeCount = ExchangeService.getStatusChangeCount(status);
    367                         if (changeCount > 0) {
    368                             errorMap.remove(serverId);
    369                         } else if (changeCount == 0) {
    370                             // This means that a ping reported changes in error; we keep a
    371                             // count of consecutive errors of this kind
    372                             String name = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN);
    373                             Integer failures = errorMap.get(serverId);
    374                             if (failures == null) {
    375                                 userLog("Last ping reported changes in error for: ", name);
    376                                 errorMap.put(serverId, 1);
    377                             } else if (failures > MAX_PING_FAILURES) {
    378                                 // We'll back off of push for this box
    379                                 pushFallback(c.getLong(Mailbox.CONTENT_ID_COLUMN));
    380                                 continue;
    381                             } else {
    382                                 userLog("Last ping reported changes in error for: ", name);
    383                                 errorMap.put(serverId, failures + 1);
    384                             }
    385                         }
    386                     }
    387                     */
    388                 if (c.moveToFirst()) {
    389                     final long mailboxId = c.getLong(Mailbox.CONTENT_ID_COLUMN);
    390                     final int contentType = c.getInt(Mailbox.CONTENT_TYPE_COLUMN);
    391                     mailboxIds.add(mailboxId);
    392                     contentTypes.add(contentType);
    393                 }
    394             } finally {
    395                 c.close();
    396             }
    397         }
    398 
    399         for (final int type : contentTypes) {
    400             switch (type) {
    401                 case Mailbox.TYPE_CALENDAR:
    402                 case Mailbox.TYPE_CONTACTS:
    403                     // Ask for a no-op sync so that we'll see calendar or contacts
    404                     // syncing in settings.
    405                     requestNoOpSync(mAmAccount, Mailbox.getAuthority(type));
    406                 default:
    407                     // Do nothing, we're already doing an Email sync.
    408             }
    409         }
    410         // Ask the EmailSyncAdapter to sync all of these mailboxes, whether they're regular
    411         // mailboxes or calendar or contacts.
    412         requestSyncForMailboxes(mAmAccount, mailboxIds);
    413     }
    414 
    415     /**
    416      * Issue a {@link ContentResolver#requestSync} to trigger a FolderSync for an account.
    417      */
    418     private void requestFolderSync() {
    419         final Bundle extras = new Bundle(1);
    420         extras.putBoolean(Mailbox.SYNC_EXTRA_ACCOUNT_ONLY, true);
    421         ContentResolver.requestSync(mAmAccount, EmailContent.AUTHORITY, extras);
    422         LogUtils.i(LOG_TAG, "requestFolderSync EasOperation %s, %s",
    423                 mAmAccount.toString(), extras.toString());
    424     }
    425 
    426     public static void requestPing(final android.accounts.Account amAccount) {
    427         final Bundle extras = new Bundle(1);
    428         extras.putBoolean(Mailbox.SYNC_EXTRA_PUSH_ONLY, true);
    429         ContentResolver.requestSync(amAccount, EmailContent.AUTHORITY, extras);
    430         LogUtils.i(LOG_TAG, "requestPing EasOperation %s, %s",
    431                 amAccount.toString(), extras.toString());
    432     }
    433 
    434 }
    435