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