EasSyncHandler.java revision 1df4a493b2efa34fce4bd8a70aca57203b4ed037
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
17package com.android.exchange.service;
18
19import android.content.ContentResolver;
20import android.content.Context;
21import android.content.SyncResult;
22import android.net.TrafficStats;
23import android.os.Bundle;
24import android.text.format.DateUtils;
25
26import com.android.emailcommon.TrafficFlags;
27import com.android.emailcommon.provider.Account;
28import com.android.emailcommon.provider.Mailbox;
29import com.android.exchange.CommandStatusException;
30import com.android.exchange.Eas;
31import com.android.exchange.EasResponse;
32import com.android.exchange.adapter.AbstractSyncParser;
33import com.android.exchange.adapter.Parser;
34import com.android.exchange.adapter.Serializer;
35import com.android.exchange.adapter.Tags;
36import com.android.exchange.eas.EasProvision;
37import com.android.mail.utils.LogUtils;
38
39import org.apache.http.HttpStatus;
40
41import java.io.IOException;
42import java.io.InputStream;
43
44/**
45 * Base class for syncing a single collection from an Exchange server. A "collection" is a single
46 * mailbox, or contacts for an account, or calendar for an account. (Tasks is part of the protocol
47 * but not implemented.)
48 * A single {@link ContentResolver#requestSync} for a single collection corresponds to a single
49 * object (of the appropriate subclass) being created and {@link #performSync} being called on it.
50 * This in turn will result in one or more Sync POST requests being sent to the Exchange server;
51 * from the client's point of view, these multiple Exchange Sync requests are all part of the same
52 * "sync" (i.e. the fact that there are multiple requests to the server is a detail of the Exchange
53 * protocol).
54 * Different collection types (e.g. mail, contacts, calendar) should subclass this class and
55 * implement the various abstract functions. The majority of how the sync flow is common to all,
56 * aside from a few details and the {@link Parser} used.
57 * Details on how this class (and Exchange Sync) works:
58 * - Overview MSDN link: http://msdn.microsoft.com/en-us/library/ee159766(v=exchg.80).aspx
59 * - Sync MSDN link: http://msdn.microsoft.com/en-us/library/gg675638(v=exchg.80).aspx
60 * - The very first time, the client sends a Sync request with SyncKey = 0 and no other parameters.
61 *   This initial Sync request simply gets us a real SyncKey.
62 *   TODO: We should add the initial Sync to EasAccountSyncHandler.
63 * - Non-initial Sync requests can be for one or more collections; this implementation does one at
64 *   a time. TODO: allow sync for multiple collections to be aggregated?
65 * - For each collection, we send SyncKey, ServerId, other modifiers, Options, and Commands. The
66 *   protocol has a specific order in which these elements must appear in the request.
67 * - {@link #buildEasRequest} forms the XML for the request, using {@link #setInitialSyncOptions},
68 *   {@link #setNonInitialSyncOptions}, and {@link #setUpsyncCommands} to fill in the details
69 *   specific for each collection type.
70 * - The Sync response may specify that there's more data available on the server, in which case
71 *   we keep sending Sync requests to get that data.
72 * - The ordering constraints and other details may require subclasses to have member variables to
73 *   store state between the various calls while performing a single Sync request. These may need
74 *   to be reset between Sync requests to the Exchange server. Additionally, there are possibly
75 *   other necessary cleanups after parsing a Sync response. These are handled in {@link #cleanup}.
76 */
77public abstract class EasSyncHandler extends EasServerConnection {
78    private static final String TAG = Eas.LOG_TAG;
79
80    /** Window sizes for PIM (contact & calendar) sync options. */
81    public static final int PIM_WINDOW_SIZE_CONTACTS = 10;
82    public static final int PIM_WINDOW_SIZE_CALENDAR = 10;
83
84    // TODO: For each type of failure, provide info about why.
85    protected static final int SYNC_RESULT_FAILED = -1;
86    protected static final int SYNC_RESULT_DONE = 0;
87    protected static final int SYNC_RESULT_MORE_AVAILABLE = 1;
88
89    /** Maximum number of Sync requests we'll send to the Exchange server in one sync attempt. */
90    private static final int MAX_LOOPING_COUNT = 100;
91
92    protected final ContentResolver mContentResolver;
93    protected final Mailbox mMailbox;
94    protected final Bundle mSyncExtras;
95    protected final SyncResult mSyncResult;
96
97    protected EasSyncHandler(final Context context, final ContentResolver contentResolver,
98            final Account account, final Mailbox mailbox, final Bundle syncExtras,
99            final SyncResult syncResult) {
100        super(context, account);
101        mContentResolver = contentResolver;
102        mMailbox = mailbox;
103        mSyncExtras = syncExtras;
104        mSyncResult = syncResult;
105    }
106
107    /**
108     * Create an instance of the appropriate subclass to handle sync for mailbox.
109     * @param context
110     * @param contentResolver
111     * @param accountManagerAccount The {@link android.accounts.Account} for this sync.
112     * @param account The {@link Account} for mailbox.
113     * @param mailbox The {@link Mailbox} to sync.
114     * @param syncExtras The extras for this sync, for consumption by {@link #performSync}.
115     * @param syncResult The output results for this sync, which may be written to by
116     *      {@link #performSync}.
117     * @return An appropriate EasSyncHandler for this mailbox, or null if this sync can't be
118     *      handled.
119     */
120    public static EasSyncHandler getEasSyncHandler(final Context context,
121            final ContentResolver contentResolver,
122            final android.accounts.Account accountManagerAccount,
123            final Account account, final Mailbox mailbox,
124            final Bundle syncExtras, final SyncResult syncResult) {
125        if (account != null && mailbox != null) {
126            switch (mailbox.mType) {
127                case Mailbox.TYPE_INBOX:
128                case Mailbox.TYPE_MAIL:
129                case Mailbox.TYPE_DRAFTS:
130                case Mailbox.TYPE_SENT:
131                case Mailbox.TYPE_TRASH:
132                    return new EasMailboxSyncHandler(context, contentResolver, account, mailbox,
133                            syncExtras, syncResult);
134                case Mailbox.TYPE_CALENDAR:
135                    return new EasCalendarSyncHandler(context, contentResolver,
136                            accountManagerAccount, account, mailbox, syncExtras, syncResult);
137                case Mailbox.TYPE_CONTACTS:
138                    return new EasContactsSyncHandler(context, contentResolver,
139                            accountManagerAccount, account, mailbox, syncExtras, syncResult);
140            }
141        }
142        // Unknown mailbox type.
143        return null;
144    }
145
146    // Interface for subclasses to implement:
147    // Subclasses must implement the abstract functions below to provide the information needed by
148    // performSync.
149
150    /**
151     * Get the flag for traffic bookkeeping for this sync type.
152     * @return The appropriate value from {@link TrafficFlags} for this sync.
153     */
154    protected abstract int getTrafficFlag();
155
156    /**
157     * Get the sync key for this mailbox.
158     * @return The sync key for the object being synced. "0" means this is the first sync. If
159     *      there is an error in getting the sync key, this function returns null.
160     */
161    protected String getSyncKey() {
162        if (mMailbox == null) {
163            return null;
164        }
165        if (mMailbox.mSyncKey == null) {
166            mMailbox.mSyncKey = "0";
167        }
168        return mMailbox.mSyncKey;
169    }
170
171    /**
172     * Get the folder class name for this mailbox.
173     * @return The string for this folder class, as defined by the Exchange spec.
174     */
175    // TODO: refactor this to be the same strings as EasPingSyncHandler#handleOneMailbox.
176    protected abstract String getFolderClassName();
177
178    /**
179     * Return an {@link AbstractSyncParser} appropriate for this sync type and response.
180     * @param is The {@link InputStream} for the {@link EasResponse} for this sync.
181     * @return The {@link AbstractSyncParser} for this response.
182     * @throws IOException
183     */
184    protected abstract AbstractSyncParser getParser(final InputStream is) throws IOException;
185
186    /**
187     * Add to the {@link Serializer} for this sync the child elements of a Collection needed for an
188     * initial sync for this collection.
189     * @param s The {@link Serializer} for this sync.
190     * @throws IOException
191     */
192    protected abstract void setInitialSyncOptions(final Serializer s) throws IOException;
193
194    /**
195     * Add to the {@link Serializer} for this sync the child elements of a Collection needed for a
196     * non-initial sync for this collection, OTHER THAN Commands (which are written by
197     * {@link #setUpsyncCommands}.
198     * @param s The {@link Serializer} for this sync.
199     * @throws IOException
200     */
201    protected abstract void setNonInitialSyncOptions(final Serializer s) throws IOException;
202
203    /**
204     * Add all Commands to the {@link Serializer} for this Sync request. Strictly speaking, it's
205     * not all Upsync requests since Fetch is also a command, but largely that's what this section
206     * is used for.
207     * @param s The {@link Serializer} for this sync.
208     * @throws IOException
209     */
210    protected abstract void setUpsyncCommands(final Serializer s) throws IOException;
211
212    /**
213     * Perform any necessary cleanup after processing a Sync response.
214     */
215    protected abstract void cleanup(final int syncResult);
216
217    // End of abstract functions.
218
219    /**
220     * Shared non-initial sync options for PIM (contacts & calendar) objects.
221     *
222     * @param s The {@link com.android.exchange.adapter.Serializer} for this sync request.
223     * @param filter The lookback to use, or null if no lookback is desired.
224     * @param windowSize
225     * @throws IOException
226     */
227    protected void setPimSyncOptions(final Serializer s, final String filter, int windowSize)
228            throws IOException {
229        s.tag(Tags.SYNC_DELETES_AS_MOVES);
230        s.tag(Tags.SYNC_GET_CHANGES);
231        s.data(Tags.SYNC_WINDOW_SIZE, String.valueOf(windowSize));
232        s.start(Tags.SYNC_OPTIONS);
233        // Set the filter (lookback), if provided
234        if (filter != null) {
235            s.data(Tags.SYNC_FILTER_TYPE, filter);
236        }
237        // Set the truncation amount and body type
238        if (getProtocolVersion() >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
239            s.start(Tags.BASE_BODY_PREFERENCE);
240            // Plain text
241            s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_TEXT);
242            s.data(Tags.BASE_TRUNCATION_SIZE, Eas.EAS12_TRUNCATION_SIZE);
243            s.end();
244        } else {
245            s.data(Tags.SYNC_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE);
246        }
247        s.end();
248    }
249
250    /**
251     * Create and populate the {@link Serializer} for this Sync POST to the Exchange server.
252     * @param syncKey The sync key to use for this request.
253     * @param initialSync Whether this sync is the first for this object.
254     * @return The {@link Serializer} for to use for this request.
255     * @throws IOException
256     */
257    private Serializer buildEasRequest(final String syncKey, final boolean initialSync)
258            throws IOException {
259        final String className = getFolderClassName();
260        LogUtils.i(TAG, "Syncing account %d mailbox %d (class %s) with syncKey %s", mAccount.mId,
261                mMailbox.mId, className, syncKey);
262
263        final Serializer s = new Serializer();
264
265        s.start(Tags.SYNC_SYNC);
266        s.start(Tags.SYNC_COLLECTIONS);
267        s.start(Tags.SYNC_COLLECTION);
268        // The "Class" element is removed in EAS 12.1 and later versions
269        if (getProtocolVersion() < Eas.SUPPORTED_PROTOCOL_EX2007_SP1_DOUBLE) {
270            s.data(Tags.SYNC_CLASS, className);
271        }
272        s.data(Tags.SYNC_SYNC_KEY, syncKey);
273        s.data(Tags.SYNC_COLLECTION_ID, mMailbox.mServerId);
274        if (initialSync) {
275            setInitialSyncOptions(s);
276        } else {
277            setNonInitialSyncOptions(s);
278            setUpsyncCommands(s);
279        }
280        s.end().end().end().done();
281
282        return s;
283    }
284
285    /**
286     * Interpret a successful (HTTP code = 200) response from the Exchange server.
287     * @param resp The {@link EasResponse} for the Sync message.
288     * @return One of {@link #SYNC_RESULT_FAILED}, {@link #SYNC_RESULT_MORE_AVAILABLE}, or
289     *      {@link #SYNC_RESULT_DONE} as appropriate for the server response.
290     */
291    private int parse(final EasResponse resp) {
292        try {
293            final AbstractSyncParser parser = getParser(resp.getInputStream());
294            final boolean moreAvailable = parser.parse();
295            if (moreAvailable) {
296                return SYNC_RESULT_MORE_AVAILABLE;
297            }
298        } catch (final Parser.EmptyStreamException e) {
299            // This indicates a compressed response which was empty, which is OK.
300        } catch (final IOException e) {
301            return SYNC_RESULT_FAILED;
302        } catch (final CommandStatusException e) {
303            return SYNC_RESULT_FAILED;
304        }
305        return SYNC_RESULT_DONE;
306    }
307
308    /**
309     * Send one Sync POST to the Exchange server, and handle the response.
310     * @return One of {@link #SYNC_RESULT_FAILED}, {@link #SYNC_RESULT_MORE_AVAILABLE}, or
311     *      {@link #SYNC_RESULT_DONE} as appropriate for the server response.
312     * @param syncResult
313     */
314    private int performOneSync(SyncResult syncResult) {
315        final String syncKey = getSyncKey();
316        if (syncKey == null) {
317            return SYNC_RESULT_FAILED;
318        }
319        final boolean initialSync = syncKey.equals("0");
320
321        final EasResponse resp;
322        try {
323            final Serializer s = buildEasRequest(syncKey, initialSync);
324            final long timeout = initialSync ? 120 * DateUtils.SECOND_IN_MILLIS : COMMAND_TIMEOUT;
325            resp = sendHttpClientPost("Sync", s.toByteArray(), timeout);
326        } catch (final IOException e) {
327            LogUtils.e(TAG, "Sync error: ", e);
328            syncResult.stats.numIoExceptions++;
329            return SYNC_RESULT_FAILED;
330        }
331
332        final int result;
333        try {
334            final int code = resp.getStatus();
335            if (code == HttpStatus.SC_OK) {
336                // A successful sync can have an empty response -- this indicates no change.
337                // In the case of a compressed stream, resp will be non-empty, but parse() handles
338                // that case.
339                if (!resp.isEmpty()) {
340                    result = parse(resp);
341                } else {
342                    result = SYNC_RESULT_DONE;
343                }
344            } else {
345                LogUtils.e(TAG, "Sync failed with Status: " + code);
346                if (resp.isProvisionError()) {
347                    final EasProvision provision = new EasProvision(mContext, mAccount.mId, this);
348                    if (provision.provision(syncResult, mAccount.mId)) {
349                        // We handled the provisioning error, so loop.
350                        result = SYNC_RESULT_MORE_AVAILABLE;
351                    } else {
352                        syncResult.stats.numAuthExceptions++;
353                        return SYNC_RESULT_FAILED; // TODO: Handle SyncStatus.FAILURE_SECURITY;
354                    }
355                } else if (resp.isAuthError()) {
356                    syncResult.stats.numAuthExceptions++;
357                    return SYNC_RESULT_FAILED; // TODO: Handle SyncStatus.FAILURE_LOGIN;
358                } else {
359                    syncResult.stats.numParseExceptions++;
360                    return SYNC_RESULT_FAILED; // TODO: Handle SyncStatus.FAILURE_OTHER;
361                }
362            }
363        } finally {
364            resp.close();
365        }
366
367        cleanup(result);
368
369        if (initialSync && result != SYNC_RESULT_FAILED) {
370            // TODO: Handle Automatic Lookback
371        }
372
373        return result;
374    }
375
376    /**
377     * Perform the sync, updating {@link #mSyncResult} as appropriate (which was passed in from
378     * the system SyncManager and will be read by it on the way out).
379     * This function can send multiple Sync messages to the Exchange server, up to
380     * {@link #MAX_LOOPING_COUNT}, due to the server replying to a Sync request with MoreAvailable.
381     * In the case of errors, this function should not attempt any retries, but rather should
382     * set {@link #mSyncResult} to reflect the problem and let the system SyncManager handle
383     * any it.
384     * @param syncResult
385     */
386    public final void performSync(SyncResult syncResult) {
387        // Set up traffic stats bookkeeping.
388        final int trafficFlags = TrafficFlags.getSyncFlags(mContext, mAccount);
389        TrafficStats.setThreadStatsTag(trafficFlags | getTrafficFlag());
390
391        // TODO: Properly handle UI status updates.
392        //syncMailboxStatus(EmailServiceStatus.IN_PROGRESS, 0);
393        int result = SYNC_RESULT_MORE_AVAILABLE;
394        int loopingCount = 0;
395        String key = getSyncKey();
396        while (result == SYNC_RESULT_MORE_AVAILABLE && loopingCount < MAX_LOOPING_COUNT) {
397            result = performOneSync(syncResult);
398            // TODO: Clear pending request queue.
399            ++loopingCount;
400            final String newKey = getSyncKey();
401            if (result == SYNC_RESULT_MORE_AVAILABLE && key.equals(newKey)) {
402                LogUtils.e(TAG,
403                        "Server has more data but we have the same key: %s loopingCount: %d",
404                        key, loopingCount);
405            }
406            key = newKey;
407        }
408        if (result == SYNC_RESULT_MORE_AVAILABLE) {
409            // TODO: Signal caller that it probably wants to sync again.
410        }
411    }
412}
413