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