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