Java tutorial
/* * 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; } }