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 
     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  */
     77 public abstract class EasSyncHandler extends EasServerConnection {
     78     private static final String TAG = Eas.LOG_TAG;
     79 
     80     public static final int MAX_WINDOW_SIZE = 512;
     81 
     82     /** Window sizes for PIM (contact & calendar) sync options. */
     83     public static final int PIM_WINDOW_SIZE_CONTACTS = 10;
     84     public static final int PIM_WINDOW_SIZE_CALENDAR = 10;
     85 
     86     // TODO: For each type of failure, provide info about why.
     87     protected static final int SYNC_RESULT_FAILED = -1;
     88     protected static final int SYNC_RESULT_DONE = 0;
     89     protected static final int SYNC_RESULT_MORE_AVAILABLE = 1;
     90 
     91     /** Maximum number of Sync requests we'll send to the Exchange server in one sync attempt. */
     92     private static final int MAX_LOOPING_COUNT = 100;
     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             return SYNC_RESULT_FAILED;
    312         }
    313         return SYNC_RESULT_DONE;
    314     }
    315 
    316     /**
    317      * Send one Sync POST to the Exchange server, and handle the response.
    318      * @return One of {@link #SYNC_RESULT_FAILED}, {@link #SYNC_RESULT_MORE_AVAILABLE}, or
    319      *      {@link #SYNC_RESULT_DONE} as appropriate for the server response.
    320      * @param syncResult
    321      * @param numWindows
    322      */
    323     private int performOneSync(SyncResult syncResult, int numWindows) {
    324         final String syncKey = getSyncKey();
    325         if (syncKey == null) {
    326             return SYNC_RESULT_FAILED;
    327         }
    328         final boolean initialSync = syncKey.equals("0");
    329 
    330         final EasResponse resp;
    331         try {
    332             final Serializer s = buildEasRequest(syncKey, initialSync, numWindows);
    333             final long timeout = initialSync ? 120 * DateUtils.SECOND_IN_MILLIS : COMMAND_TIMEOUT;
    334             resp = sendHttpClientPost("Sync", s.toByteArray(), timeout);
    335         } catch (final IOException e) {
    336             LogUtils.e(TAG, e, "Sync error:");
    337             syncResult.stats.numIoExceptions++;
    338             return SYNC_RESULT_FAILED;
    339         }
    340 
    341         final int result;
    342         try {
    343             final int code = resp.getStatus();
    344             if (code == HttpStatus.SC_OK) {
    345                 // A successful sync can have an empty response -- this indicates no change.
    346                 // In the case of a compressed stream, resp will be non-empty, but parse() handles
    347                 // that case.
    348                 if (!resp.isEmpty()) {
    349                     result = parse(resp);
    350                 } else {
    351                     result = SYNC_RESULT_DONE;
    352                 }
    353             } else {
    354                 LogUtils.e(TAG, "Sync failed with Status: " + code);
    355                 if (resp.isProvisionError()) {
    356                     final EasProvision provision = new EasProvision(mContext, mAccount.mId, this);
    357                     if (provision.provision(syncResult, mAccount.mId)) {
    358                         // We handled the provisioning error, so loop.
    359                         result = SYNC_RESULT_MORE_AVAILABLE;
    360                     } else {
    361                         syncResult.stats.numAuthExceptions++;
    362                         return SYNC_RESULT_FAILED; // TODO: Handle SyncStatus.FAILURE_SECURITY;
    363                     }
    364                 } else if (resp.isAuthError()) {
    365                     syncResult.stats.numAuthExceptions++;
    366                     return SYNC_RESULT_FAILED; // TODO: Handle SyncStatus.FAILURE_LOGIN;
    367                 } else {
    368                     syncResult.stats.numParseExceptions++;
    369                     return SYNC_RESULT_FAILED; // TODO: Handle SyncStatus.FAILURE_OTHER;
    370                 }
    371             }
    372         } finally {
    373             resp.close();
    374         }
    375 
    376         cleanup(result);
    377 
    378         if (initialSync && result != SYNC_RESULT_FAILED) {
    379             // TODO: Handle Automatic Lookback
    380         }
    381 
    382         return result;
    383     }
    384 
    385     /**
    386      * Perform the sync, updating {@link #mSyncResult} as appropriate (which was passed in from
    387      * the system SyncManager and will be read by it on the way out).
    388      * This function can send multiple Sync messages to the Exchange server, up to
    389      * {@link #MAX_LOOPING_COUNT}, due to the server replying to a Sync request with MoreAvailable.
    390      * In the case of errors, this function should not attempt any retries, but rather should
    391      * set {@link #mSyncResult} to reflect the problem and let the system SyncManager handle
    392      * any it.
    393      * @param syncResult
    394      */
    395     public final void performSync(SyncResult syncResult) {
    396         // Set up traffic stats bookkeeping.
    397         final int trafficFlags = TrafficFlags.getSyncFlags(mContext, mAccount);
    398         TrafficStats.setThreadStatsTag(trafficFlags | getTrafficFlag());
    399 
    400         // TODO: Properly handle UI status updates.
    401         //syncMailboxStatus(EmailServiceStatus.IN_PROGRESS, 0);
    402         int result = SYNC_RESULT_MORE_AVAILABLE;
    403         int numWindows = 0;
    404         String key = getSyncKey();
    405         while (result == SYNC_RESULT_MORE_AVAILABLE) {
    406             result = performOneSync(syncResult, numWindows);
    407             // TODO: Clear pending request queue.
    408             ++numWindows;
    409             final String newKey = getSyncKey();
    410             if (result == SYNC_RESULT_MORE_AVAILABLE && key.equals(newKey)) {
    411                 LogUtils.e(TAG,
    412                         "Server has more data but we have the same key: %s numWindows: %d",
    413                         key, numWindows);
    414                 numWindows++;
    415             } else {
    416                 numWindows = 1;
    417             }
    418             key = newKey;
    419         }
    420     }
    421 }
    422