com.android.exchange.service.EasSyncHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.android.exchange.service.EasSyncHandler.java

Source

/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.exchange.service;

import android.content.ContentResolver;
import android.content.Context;
import android.content.SyncResult;
import android.net.TrafficStats;
import android.os.Bundle;
import android.text.format.DateUtils;

import com.android.emailcommon.TrafficFlags;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.Mailbox;
import com.android.exchange.CommandStatusException;
import com.android.exchange.Eas;
import com.android.exchange.EasResponse;
import com.android.exchange.adapter.AbstractSyncParser;
import com.android.exchange.adapter.Parser;
import com.android.exchange.adapter.Serializer;
import com.android.exchange.adapter.Tags;
import com.android.exchange.eas.EasProvision;
import com.android.mail.utils.LogUtils;

import org.apache.http.HttpStatus;

import java.io.IOException;
import java.io.InputStream;
import java.security.cert.CertificateException;

/**
 * Base class for syncing a single collection from an Exchange server. A "collection" is a single
 * mailbox, or contacts for an account, or calendar for an account. (Tasks is part of the protocol
 * but not implemented.)
 * A single {@link ContentResolver#requestSync} for a single collection corresponds to a single
 * object (of the appropriate subclass) being created and {@link #performSync} being called on it.
 * This in turn will result in one or more Sync POST requests being sent to the Exchange server;
 * from the client's point of view, these multiple Exchange Sync requests are all part of the same
 * "sync" (i.e. the fact that there are multiple requests to the server is a detail of the Exchange
 * protocol).
 * Different collection types (e.g. mail, contacts, calendar) should subclass this class and
 * implement the various abstract functions. The majority of how the sync flow is common to all,
 * aside from a few details and the {@link Parser} used.
 * Details on how this class (and Exchange Sync) works:
 * - Overview MSDN link: http://msdn.microsoft.com/en-us/library/ee159766(v=exchg.80).aspx
 * - Sync MSDN link: http://msdn.microsoft.com/en-us/library/gg675638(v=exchg.80).aspx
 * - The very first time, the client sends a Sync request with SyncKey = 0 and no other parameters.
 *   This initial Sync request simply gets us a real SyncKey.
 *   TODO: We should add the initial Sync to EasAccountSyncHandler.
 * - Non-initial Sync requests can be for one or more collections; this implementation does one at
 *   a time. TODO: allow sync for multiple collections to be aggregated?
 * - For each collection, we send SyncKey, ServerId, other modifiers, Options, and Commands. The
 *   protocol has a specific order in which these elements must appear in the request.
 * - {@link #buildEasRequest} forms the XML for the request, using {@link #setInitialSyncOptions},
 *   {@link #setNonInitialSyncOptions}, and {@link #setUpsyncCommands} to fill in the details
 *   specific for each collection type.
 * - The Sync response may specify that there's more data available on the server, in which case
 *   we keep sending Sync requests to get that data.
 * - The ordering constraints and other details may require subclasses to have member variables to
 *   store state between the various calls while performing a single Sync request. These may need
 *   to be reset between Sync requests to the Exchange server. Additionally, there are possibly
 *   other necessary cleanups after parsing a Sync response. These are handled in {@link #cleanup}.
 */
public abstract class EasSyncHandler extends EasServerConnection {
    private static final String TAG = Eas.LOG_TAG;

    public static final int MAX_WINDOW_SIZE = 512;

    /** Window sizes for PIM (contact & calendar) sync options. */
    public static final int PIM_WINDOW_SIZE_CONTACTS = 10;
    public static final int PIM_WINDOW_SIZE_CALENDAR = 10;

    // TODO: For each type of failure, provide info about why.
    protected static final int SYNC_RESULT_DENIED = -3;
    protected static final int SYNC_RESULT_PROVISIONING_ERROR = -2;
    protected static final int SYNC_RESULT_FAILED = -1;
    protected static final int SYNC_RESULT_DONE = 0;
    protected static final int SYNC_RESULT_MORE_AVAILABLE = 1;

    protected final ContentResolver mContentResolver;
    protected final Mailbox mMailbox;
    protected final Bundle mSyncExtras;
    protected final SyncResult mSyncResult;

    protected EasSyncHandler(final Context context, final ContentResolver contentResolver, final Account account,
            final Mailbox mailbox, final Bundle syncExtras, final SyncResult syncResult) {
        super(context, account);
        mContentResolver = contentResolver;
        mMailbox = mailbox;
        mSyncExtras = syncExtras;
        mSyncResult = syncResult;
    }

    /**
     * Create an instance of the appropriate subclass to handle sync for mailbox.
     * @param context
     * @param contentResolver
     * @param accountManagerAccount The {@link android.accounts.Account} for this sync.
     * @param account The {@link Account} for mailbox.
     * @param mailbox The {@link Mailbox} to sync.
     * @param syncExtras The extras for this sync, for consumption by {@link #performSync}.
     * @param syncResult The output results for this sync, which may be written to by
     *      {@link #performSync}.
     * @return An appropriate EasSyncHandler for this mailbox, or null if this sync can't be
     *      handled.
     */
    public static EasSyncHandler getEasSyncHandler(final Context context, final ContentResolver contentResolver,
            final android.accounts.Account accountManagerAccount, final Account account, final Mailbox mailbox,
            final Bundle syncExtras, final SyncResult syncResult) {
        if (account != null && mailbox != null) {
            switch (mailbox.mType) {
            case Mailbox.TYPE_INBOX:
            case Mailbox.TYPE_MAIL:
            case Mailbox.TYPE_DRAFTS:
            case Mailbox.TYPE_SENT:
            case Mailbox.TYPE_TRASH:
                return new EasMailboxSyncHandler(context, contentResolver, account, mailbox, syncExtras,
                        syncResult);
            case Mailbox.TYPE_CALENDAR:
                return new EasCalendarSyncHandler(context, contentResolver, accountManagerAccount, account, mailbox,
                        syncExtras, syncResult);
            case Mailbox.TYPE_CONTACTS:
                return new EasContactsSyncHandler(context, contentResolver, accountManagerAccount, account, mailbox,
                        syncExtras, syncResult);
            }
        }
        // Unknown mailbox type.
        LogUtils.e(TAG, "Invalid mailbox type %d", mailbox.mType);
        return null;
    }

    // Interface for subclasses to implement:
    // Subclasses must implement the abstract functions below to provide the information needed by
    // performSync.

    /**
     * Get the flag for traffic bookkeeping for this sync type.
     * @return The appropriate value from {@link TrafficFlags} for this sync.
     */
    protected abstract int getTrafficFlag();

    /**
     * Get the sync key for this mailbox.
     * @return The sync key for the object being synced. "0" means this is the first sync. If
     *      there is an error in getting the sync key, this function returns null.
     */
    protected String getSyncKey() {
        if (mMailbox == null) {
            return null;
        }
        if (mMailbox.mSyncKey == null) {
            mMailbox.mSyncKey = "0";
        }
        return mMailbox.mSyncKey;
    }

    /**
     * Get the folder class name for this mailbox.
     * @return The string for this folder class, as defined by the Exchange spec.
     */
    // TODO: refactor this to be the same strings as EasPingSyncHandler#handleOneMailbox.
    protected abstract String getFolderClassName();

    /**
     * Return an {@link AbstractSyncParser} appropriate for this sync type and response.
     * @param is The {@link InputStream} for the {@link EasResponse} for this sync.
     * @return The {@link AbstractSyncParser} for this response.
     * @throws IOException
     */
    protected abstract AbstractSyncParser getParser(final InputStream is) throws IOException;

    /**
     * Add to the {@link Serializer} for this sync the child elements of a Collection needed for an
     * initial sync for this collection.
     * @param s The {@link Serializer} for this sync.
     * @throws IOException
     */
    protected abstract void setInitialSyncOptions(final Serializer s) throws IOException;

    /**
     * Add to the {@link Serializer} for this sync the child elements of a Collection needed for a
     * non-initial sync for this collection, OTHER THAN Commands (which are written by
     * {@link #setUpsyncCommands}.
     *
     * @param s The {@link com.android.exchange.adapter.Serializer} for this sync.
     * @param numWindows
     * @throws IOException
     */
    protected abstract void setNonInitialSyncOptions(final Serializer s, int numWindows) throws IOException;

    /**
     * Add all Commands to the {@link Serializer} for this Sync request. Strictly speaking, it's
     * not all Upsync requests since Fetch is also a command, but largely that's what this section
     * is used for.
     * @param s The {@link Serializer} for this sync.
     * @throws IOException
     */
    protected abstract void setUpsyncCommands(final Serializer s) throws IOException;

    /**
     * Perform any necessary cleanup after processing a Sync response.
     */
    protected abstract void cleanup(final int syncResult);

    // End of abstract functions.

    /**
     * Shared non-initial sync options for PIM (contacts & calendar) objects.
     *
     * @param s The {@link com.android.exchange.adapter.Serializer} for this sync request.
     * @param filter The lookback to use, or null if no lookback is desired.
     * @param windowSize
     * @throws IOException
     */
    protected void setPimSyncOptions(final Serializer s, final String filter, int windowSize) throws IOException {
        s.tag(Tags.SYNC_DELETES_AS_MOVES);
        s.tag(Tags.SYNC_GET_CHANGES);
        s.data(Tags.SYNC_WINDOW_SIZE, String.valueOf(windowSize));
        s.start(Tags.SYNC_OPTIONS);
        // Set the filter (lookback), if provided
        if (filter != null) {
            s.data(Tags.SYNC_FILTER_TYPE, filter);
        }
        // Set the truncation amount and body type
        if (getProtocolVersion() >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
            s.start(Tags.BASE_BODY_PREFERENCE);
            // Plain text
            s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_TEXT);
            s.data(Tags.BASE_TRUNCATION_SIZE, Eas.EAS12_TRUNCATION_SIZE);
            s.end();
        } else {
            s.data(Tags.SYNC_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE);
        }
        s.end();
    }

    /**
     * Create and populate the {@link Serializer} for this Sync POST to the Exchange server.
     *
     * @param syncKey The sync key to use for this request.
     * @param initialSync Whether this sync is the first for this object.
     * @param numWindows
     * @return The {@link Serializer} for to use for this request.
     * @throws IOException
     */
    private Serializer buildEasRequest(final String syncKey, final boolean initialSync, int numWindows)
            throws IOException {
        final String className = getFolderClassName();
        LogUtils.d(TAG, "Syncing account %d mailbox %d (class %s) with syncKey %s", mAccount.mId, mMailbox.mId,
                className, syncKey);

        final Serializer s = new Serializer();

        s.start(Tags.SYNC_SYNC);
        s.start(Tags.SYNC_COLLECTIONS);
        s.start(Tags.SYNC_COLLECTION);
        // The "Class" element is removed in EAS 12.1 and later versions
        if (getProtocolVersion() < Eas.SUPPORTED_PROTOCOL_EX2007_SP1_DOUBLE) {
            s.data(Tags.SYNC_CLASS, className);
        }
        s.data(Tags.SYNC_SYNC_KEY, syncKey);
        s.data(Tags.SYNC_COLLECTION_ID, mMailbox.mServerId);
        if (initialSync) {
            setInitialSyncOptions(s);
        } else {
            setNonInitialSyncOptions(s, numWindows);
            setUpsyncCommands(s);
        }
        s.end().end().end().done();

        return s;
    }

    /**
     * Interpret a successful (HTTP code = 200) response from the Exchange server.
     * @param resp The {@link EasResponse} for the Sync message.
     * @return One of {@link #SYNC_RESULT_FAILED}, {@link #SYNC_RESULT_MORE_AVAILABLE}, or
     *      {@link #SYNC_RESULT_DONE} as appropriate for the server response.
     */
    private int parse(final EasResponse resp) {
        try {
            final AbstractSyncParser parser = getParser(resp.getInputStream());
            final boolean moreAvailable = parser.parse();
            if (moreAvailable) {
                return SYNC_RESULT_MORE_AVAILABLE;
            }
        } catch (final Parser.EmptyStreamException e) {
            // This indicates a compressed response which was empty, which is OK.
        } catch (final IOException e) {
            return SYNC_RESULT_FAILED;
        } catch (final CommandStatusException e) {
            // TODO: This is basically copied from EasOperation, will go away when this merges.
            final int status = e.mStatus;
            LogUtils.e(TAG, "CommandStatusException: %d", status);
            if (CommandStatusException.CommandStatus.isNeedsProvisioning(status)) {
                return SYNC_RESULT_PROVISIONING_ERROR;
            }
            if (CommandStatusException.CommandStatus.isDeniedAccess(status)) {
                return SYNC_RESULT_DENIED;
            }
            return SYNC_RESULT_FAILED;
        }
        return SYNC_RESULT_DONE;
    }

    /**
     * Send one Sync POST to the Exchange server, and handle the response.
     * @return One of {@link #SYNC_RESULT_FAILED}, {@link #SYNC_RESULT_MORE_AVAILABLE}, or
     *      {@link #SYNC_RESULT_DONE} as appropriate for the server response.
     * @param syncResult
     * @param numWindows
     */
    private int performOneSync(SyncResult syncResult, int numWindows) {
        final String syncKey = getSyncKey();
        if (syncKey == null) {
            return SYNC_RESULT_FAILED;
        }
        final boolean initialSync = syncKey.equals("0");

        final EasResponse resp;
        try {
            final Serializer s = buildEasRequest(syncKey, initialSync, numWindows);
            final long timeout = initialSync ? 120 * DateUtils.SECOND_IN_MILLIS : COMMAND_TIMEOUT;
            resp = sendHttpClientPost("Sync", s.toByteArray(), timeout);
        } catch (final IOException e) {
            LogUtils.e(TAG, e, "Sync error:");
            syncResult.stats.numIoExceptions++;
            return SYNC_RESULT_FAILED;
        } catch (final CertificateException e) {
            LogUtils.e(TAG, e, "Certificate error:");
            syncResult.stats.numAuthExceptions++;
            return SYNC_RESULT_FAILED;
        }

        final int result;
        try {
            final int responseResult;
            final int code = resp.getStatus();
            if (code == HttpStatus.SC_OK) {
                // A successful sync can have an empty response -- this indicates no change.
                // In the case of a compressed stream, resp will be non-empty, but parse() handles
                // that case.
                if (!resp.isEmpty()) {
                    responseResult = parse(resp);
                } else {
                    responseResult = SYNC_RESULT_DONE;
                }
            } else {
                LogUtils.e(TAG, "Sync failed with Status: " + code);
                responseResult = SYNC_RESULT_FAILED;
            }

            if (responseResult == SYNC_RESULT_DONE || responseResult == SYNC_RESULT_MORE_AVAILABLE) {
                result = responseResult;
            } else if (resp.isProvisionError() || responseResult == SYNC_RESULT_PROVISIONING_ERROR) {
                final EasProvision provision = new EasProvision(mContext, mAccount.mId, this);
                if (provision.provision(syncResult, mAccount.mId)) {
                    // We handled the provisioning error, so loop.
                    LogUtils.d(TAG, "Provisioning error handled during sync, retrying");
                    result = SYNC_RESULT_MORE_AVAILABLE;
                } else {
                    syncResult.stats.numAuthExceptions++;
                    result = SYNC_RESULT_FAILED;
                }
            } else if (resp.isAuthError() || responseResult == SYNC_RESULT_DENIED) {
                syncResult.stats.numAuthExceptions++;
                result = SYNC_RESULT_FAILED;
            } else {
                syncResult.stats.numParseExceptions++;
                result = SYNC_RESULT_FAILED;
            }

        } finally {
            resp.close();
        }

        cleanup(result);

        if (initialSync && result != SYNC_RESULT_FAILED) {
            // TODO: Handle Automatic Lookback
        }

        return result;
    }

    /**
     * Perform the sync, updating {@link #mSyncResult} as appropriate (which was passed in from
     * the system SyncManager and will be read by it on the way out).
     * This function can send multiple Sync messages to the Exchange server, due to the server
     * replying to a Sync request with MoreAvailable.
     * In the case of errors, this function should not attempt any retries, but rather should
     * set {@link #mSyncResult} to reflect the problem and let the system SyncManager handle
     * any it.
     * @param syncResult
     */
    public final boolean performSync(SyncResult syncResult) {
        // Set up traffic stats bookkeeping.
        final int trafficFlags = TrafficFlags.getSyncFlags(mContext, mAccount);
        TrafficStats.setThreadStatsTag(trafficFlags | getTrafficFlag());

        // TODO: Properly handle UI status updates.
        //syncMailboxStatus(EmailServiceStatus.IN_PROGRESS, 0);
        int result = SYNC_RESULT_MORE_AVAILABLE;
        int numWindows = 1;
        String key = getSyncKey();
        while (result == SYNC_RESULT_MORE_AVAILABLE) {
            result = performOneSync(syncResult, numWindows);
            // TODO: Clear pending request queue.
            final String newKey = getSyncKey();
            if (result == SYNC_RESULT_MORE_AVAILABLE && key.equals(newKey)) {
                LogUtils.e(TAG, "Server has more data but we have the same key: %s numWindows: %d", key,
                        numWindows);
                numWindows++;
            } else {
                numWindows = 1;
            }
            key = newKey;
        }
        return result == SYNC_RESULT_DONE;
    }
}