Add EasSyncCollectionTypeBase and its mail sync subclass.

Each collection type (mail, contact, calendar, and eventually more)
has certain details in the request formation and response handling
that differ by type. Rather than subclass the operation, it seems
better to put these details in a different class.

Longer term, this decoupling should make it easier to eventually
send a single sync request for all collections, rather than one
per folder. That is not on tap for this release, however.

This change adds only one subclass, for mail sync. The code is
largely taken from the EasMailboxSyncHandler, which this replaces.
The contacts and calendar subclasses will follow, as will replacing
the old EasSyncHandler set of classes.

Change-Id: I29e20faa5ff332f7af89e48c11577937fcbc983d
diff --git a/src/com/android/exchange/adapter/EmailSyncParser.java b/src/com/android/exchange/adapter/EmailSyncParser.java
index fef6c31..2d5272b 100644
--- a/src/com/android/exchange/adapter/EmailSyncParser.java
+++ b/src/com/android/exchange/adapter/EmailSyncParser.java
@@ -114,6 +114,11 @@
         }
     }
 
+    public EmailSyncParser(final Context context, final InputStream in, final Mailbox mailbox,
+            final Account account) throws IOException {
+        this(context, context.getContentResolver(), in, mailbox, account);
+    }
+
     public boolean fetchNeeded() {
         return mFetchNeeded;
     }
diff --git a/src/com/android/exchange/eas/EasSyncBase.java b/src/com/android/exchange/eas/EasSyncBase.java
index 90237dd..cf48ae1 100644
--- a/src/com/android/exchange/eas/EasSyncBase.java
+++ b/src/com/android/exchange/eas/EasSyncBase.java
@@ -2,6 +2,7 @@
 
 import android.content.Context;
 import android.text.format.DateUtils;
+import android.util.SparseArray;
 
 import com.android.emailcommon.provider.Account;
 import com.android.emailcommon.provider.EmailContent;
@@ -20,8 +21,8 @@
 import java.io.IOException;
 
 /**
- * Performs an EAS downsync operation for one folder.
- * TODO: Merge with {@link EasSync}, which currently handles upsync.
+ * Performs an EAS sync operation for one folder (excluding mail upsync).
+ * TODO: Merge with {@link EasSync}, which currently handles mail upsync.
  */
 public class EasSyncBase extends EasOperation {
 
@@ -35,6 +36,20 @@
 
     private int mNumWindows;
 
+    /**
+     * {@link EasSyncCollectionTypeBase} classes currently are stateless, so there's no need to
+     * create one per operation instance. We just maintain a single instance of each in a map and
+     * grab it as necessary.
+     */
+    private static final SparseArray<EasSyncCollectionTypeBase> COLLECTION_TYPE_HANDLERS;
+    static {
+        COLLECTION_TYPE_HANDLERS = new SparseArray<EasSyncCollectionTypeBase>(3);
+        // TODO: As the subclasses are created, add them to the map.
+        COLLECTION_TYPE_HANDLERS.put(Mailbox.TYPE_MAIL, new EasSyncMail());
+        COLLECTION_TYPE_HANDLERS.put(Mailbox.TYPE_CALENDAR, null);
+        COLLECTION_TYPE_HANDLERS.put(Mailbox.TYPE_CONTACTS, null);
+    }
+
     // TODO: Convert to accountId when ready to convert to EasService.
     public EasSyncBase(final Context context, final Account account, final Mailbox mailbox) {
         super(context, account);
@@ -70,6 +85,13 @@
         LogUtils.d(TAG, "Syncing account %d mailbox %d (class %s) with syncKey %s", mAccount.mId,
                 mMailbox.mId, className, syncKey);
 
+        final EasSyncCollectionTypeBase collectionTypeHandler =
+                getCollectionTypeHandler(mMailbox.mType);
+        if (collectionTypeHandler == null) {
+            throw new IllegalStateException("No handler for collection type: "
+                    + Integer.toString(mMailbox.mType));
+        }
+
         final Serializer s = new Serializer();
         s.start(Tags.SYNC_SYNC);
         s.start(Tags.SYNC_COLLECTIONS);
@@ -80,12 +102,8 @@
         }
         s.data(Tags.SYNC_SYNC_KEY, syncKey);
         s.data(Tags.SYNC_COLLECTION_ID, mMailbox.mServerId);
-        if (mInitialSync) {
-            //setInitialSyncOptions(s);
-        } else {
-            //setNonInitialSyncOptions(s, mNumWindows);
-            //setUpsyncCommands(s);
-        }
+        collectionTypeHandler.setSyncOptions(mContext, s, getProtocolVersion(), mAccount, mMailbox,
+                mInitialSync, mNumWindows);
         s.end().end().end().done();
 
         return makeEntity(s);
@@ -95,7 +113,9 @@
     protected int handleResponse(final EasResponse response)
             throws IOException, CommandStatusException {
         try {
-            final AbstractSyncParser parser = null;//getParser(response.getInputStream());
+            final AbstractSyncParser parser =
+                    getCollectionTypeHandler(mMailbox.mType).getParser(mContext, mAccount, mMailbox,
+                            response.getInputStream());
             final boolean moreAvailable = parser.parse();
             if (moreAvailable) {
                 return RESULT_MORE_AVAILABLE;
@@ -135,4 +155,20 @@
         return super.getTimeout();
     }
 
+    /**
+     * Get an instance of the correct {@link EasSyncCollectionTypeBase} for a specific collection
+     * type.
+     * @param type The type of the {@link Mailbox} that we're trying to sync.
+     * @return An {@link EasSyncCollectionTypeBase} appropriate for this type.
+     */
+    private static EasSyncCollectionTypeBase getCollectionTypeHandler(final int type) {
+        EasSyncCollectionTypeBase typeHandler = COLLECTION_TYPE_HANDLERS.get(type);
+        if (typeHandler == null) {
+            // Treat mail as the default; this also means we don't bother mapping every type of
+            // mail folder.
+            // TODO: Should we just do that?
+            typeHandler = COLLECTION_TYPE_HANDLERS.get(Mailbox.TYPE_MAIL);
+        }
+        return typeHandler;
+    }
 }
diff --git a/src/com/android/exchange/eas/EasSyncCollectionTypeBase.java b/src/com/android/exchange/eas/EasSyncCollectionTypeBase.java
new file mode 100644
index 0000000..6ff1508
--- /dev/null
+++ b/src/com/android/exchange/eas/EasSyncCollectionTypeBase.java
@@ -0,0 +1,55 @@
+package com.android.exchange.eas;
+
+import android.content.Context;
+
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.exchange.adapter.AbstractSyncParser;
+import com.android.exchange.adapter.Serializer;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Abstract base class that handles the details of syncing a specific collection type.
+ * These details include:
+ * - Forming the request options. Contacts, Calendar, and Mail set this up differently.
+ * - Getting the appropriate parser for this collection type.
+ * These classes should be stateless, i.e. the distinct subtypes and instances are used simply
+ * to have polymorphic behavior for these functions. If member variables are ever added to any
+ * of these classes, {@link EasSyncBase} MUST change how it creates these objects.
+ */
+public abstract class EasSyncCollectionTypeBase {
+
+    public static final int MAX_WINDOW_SIZE = 512;
+
+    /**
+     * Write the contents of a Collection node in an EAS sync request appropriate for our mailbox.
+     * See http://msdn.microsoft.com/en-us/library/gg650891(v=exchg.80).aspx for documentation on
+     * the contents of this sync request element.
+     * @param context
+     * @param s The {@link Serializer} for the current request. This should be within a
+     *          {@link com.android.exchange.adapter.Tags#SYNC_COLLECTION} element.
+     * @param protocolVersion
+     * @param account
+     * @param mailbox
+     * @param isInitialSync
+     * @param numWindows
+     * @throws IOException
+     */
+    public abstract void setSyncOptions(final Context context, final Serializer s,
+            final double protocolVersion, final Account account, final Mailbox mailbox,
+            final boolean isInitialSync, final int numWindows) throws IOException;
+
+    /**
+     * Create a parser for the current response data, appropriate for this collection type.
+     * @param context
+     * @param account
+     * @param mailbox
+     * @param is The {@link InputStream} for the server response we're processing.
+     * @return An appropriate parser for this input.
+     * @throws IOException
+     */
+    public abstract AbstractSyncParser getParser(final Context context, final Account account,
+            final Mailbox mailbox, final InputStream is) throws IOException;
+}
diff --git a/src/com/android/exchange/eas/EasSyncMail.java b/src/com/android/exchange/eas/EasSyncMail.java
new file mode 100644
index 0000000..eec5bdf
--- /dev/null
+++ b/src/com/android/exchange/eas/EasSyncMail.java
@@ -0,0 +1,165 @@
+package com.android.exchange.eas;
+
+import android.content.Context;
+import android.database.Cursor;
+
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent.Message;
+import com.android.emailcommon.provider.EmailContent.MessageColumns;
+import com.android.emailcommon.provider.EmailContent.SyncColumns;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.emailcommon.service.SyncWindow;
+import com.android.exchange.Eas;
+import com.android.exchange.adapter.AbstractSyncParser;
+import com.android.exchange.adapter.EmailSyncParser;
+import com.android.exchange.adapter.Serializer;
+import com.android.exchange.adapter.Tags;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+/**
+ * Subclass to handle sync details for mail collections.
+ */
+public class EasSyncMail extends EasSyncCollectionTypeBase {
+
+    /**
+     * The projection used for building the fetch request list.
+     */
+    private static final String[] FETCH_REQUEST_PROJECTION = { SyncColumns.SERVER_ID };
+    private static final int FETCH_REQUEST_SERVER_ID = 0;
+
+    private static final int EMAIL_WINDOW_SIZE = 10;
+
+    @Override
+    public void setSyncOptions(final Context context, final Serializer s,
+            final double protocolVersion, final Account account, final Mailbox mailbox,
+            final boolean isInitialSync, final int numWindows) throws IOException {
+        if (isInitialSync) {
+            // No special options to set for initial mailbox sync.
+            return;
+        }
+
+        // Check for messages that aren't fully loaded.
+        final ArrayList<String> messagesToFetch = addToFetchRequestList(context, mailbox);
+        // The "empty" case is typical; we send a request for changes, and also specify a sync
+        // window, body preference type (HTML for EAS 12.0 and later; MIME for EAS 2.5), and
+        // truncation
+        // If there are fetch requests, we only want the fetches (i.e. no changes from the server)
+        // so we turn MIME support off.  Note that we are always using EAS 2.5 if there are fetch
+        // requests
+        if (messagesToFetch.isEmpty()) {
+            // Permanently delete if in trash mailbox
+            // In Exchange 2003, deletes-as-moves tag = true; no tag = false
+            // In Exchange 2007 and up, deletes-as-moves tag is "0" (false) or "1" (true)
+            final boolean isTrashMailbox = mailbox.mType == Mailbox.TYPE_TRASH;
+            if (protocolVersion < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
+                if (!isTrashMailbox) {
+                    s.tag(Tags.SYNC_DELETES_AS_MOVES);
+                }
+            } else {
+                s.data(Tags.SYNC_DELETES_AS_MOVES, isTrashMailbox ? "0" : "1");
+            }
+            s.tag(Tags.SYNC_GET_CHANGES);
+
+            final int windowSize = numWindows * EMAIL_WINDOW_SIZE;
+            if (windowSize > MAX_WINDOW_SIZE  + EMAIL_WINDOW_SIZE) {
+                throw new IOException("Max window size reached and still no data");
+            }
+            s.data(Tags.SYNC_WINDOW_SIZE,
+                    String.valueOf(windowSize < MAX_WINDOW_SIZE ? windowSize : MAX_WINDOW_SIZE));
+            s.start(Tags.SYNC_OPTIONS);
+            // Set the lookback appropriately (EAS calls this a "filter")
+            s.data(Tags.SYNC_FILTER_TYPE, getEmailFilter(account, mailbox));
+            // Set the truncation amount for all classes
+            if (protocolVersion >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
+                s.start(Tags.BASE_BODY_PREFERENCE);
+                // HTML for email
+                s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_HTML);
+                s.data(Tags.BASE_TRUNCATION_SIZE, Eas.EAS12_TRUNCATION_SIZE);
+                s.end();
+            } else {
+                // Use MIME data for EAS 2.5
+                s.data(Tags.SYNC_MIME_SUPPORT, Eas.MIME_BODY_PREFERENCE_MIME);
+                s.data(Tags.SYNC_MIME_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE);
+            }
+            s.end();
+        } else {
+            // If we have any messages that are not fully loaded, ask for plain text rather than
+            // MIME, to guarantee we'll get usable text body. This also means we should NOT ask for
+            // new messages -- we only want data for the message explicitly fetched.
+            s.start(Tags.SYNC_OPTIONS);
+            s.data(Tags.SYNC_MIME_SUPPORT, Eas.MIME_BODY_PREFERENCE_TEXT);
+            s.data(Tags.SYNC_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE);
+            s.end();
+
+            // Add FETCH commands for messages that need a body (i.e. we didn't find it during our
+            // earlier sync; this happens only in EAS 2.5 where the body couldn't be found after
+            // parsing the message's MIME data).
+            s.start(Tags.SYNC_COMMANDS);
+            for (final String serverId : messagesToFetch) {
+                s.start(Tags.SYNC_FETCH).data(Tags.SYNC_SERVER_ID, serverId).end();
+            }
+            s.end();
+        }
+    }
+
+    @Override
+    public AbstractSyncParser getParser(final Context context, final Account account,
+            final Mailbox mailbox, final InputStream is) throws IOException {
+        return new EmailSyncParser(context, is, mailbox, account);
+    }
+
+    /**
+     * Query the provider for partially loaded messages.
+     * @return Server ids for partially loaded messages.
+     */
+    private ArrayList<String> addToFetchRequestList(final Context context, final Mailbox mailbox) {
+        final ArrayList<String> messagesToFetch = new ArrayList<String>();
+        final Cursor c = context.getContentResolver().query(Message.CONTENT_URI,
+                FETCH_REQUEST_PROJECTION,  MessageColumns.FLAG_LOADED + "=" +
+                Message.FLAG_LOADED_PARTIAL + " AND " +  MessageColumns.MAILBOX_KEY + "=?",
+                new String[] {Long.toString(mailbox.mId)}, null);
+        if (c != null) {
+            try {
+                while (c.moveToNext()) {
+                    messagesToFetch.add(c.getString(FETCH_REQUEST_SERVER_ID));
+                }
+            } finally {
+                c.close();
+            }
+        }
+        return messagesToFetch;
+    }
+
+    /**
+     * Get the sync window for this collection and translate it to EAS's value for that (EAS refers
+     * to this as the "filter").
+     * @param account The {@link Account} for this sync; its sync window is used if the mailbox
+     *                doesn't specify an override.
+     * @param mailbox The {@link Mailbox} for this sync.
+     * @return The EAS string value for the sync window specified for this mailbox.
+     */
+    private String getEmailFilter(final Account account, final Mailbox mailbox) {
+        final int syncLookback = mailbox.mSyncLookback == SyncWindow.SYNC_WINDOW_ACCOUNT
+                ? account.mSyncLookback : mailbox.mSyncLookback;
+        switch (syncLookback) {
+            case SyncWindow.SYNC_WINDOW_1_DAY:
+                return Eas.FILTER_1_DAY;
+            case SyncWindow.SYNC_WINDOW_3_DAYS:
+                return Eas.FILTER_3_DAYS;
+            case SyncWindow.SYNC_WINDOW_1_WEEK:
+                return Eas.FILTER_1_WEEK;
+            case SyncWindow.SYNC_WINDOW_2_WEEKS:
+                return Eas.FILTER_2_WEEKS;
+            case SyncWindow.SYNC_WINDOW_1_MONTH:
+                return Eas.FILTER_1_MONTH;
+            case SyncWindow.SYNC_WINDOW_ALL:
+                return Eas.FILTER_ALL;
+            default:
+                // Auto window is deprecated and will also use the default.
+                return Eas.FILTER_1_WEEK;
+        }
+    }
+}