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