Make search an EasOperation

This is still hooked up to EmailSyncAdapterService

Change-Id: I3194a09b50d38aa0b69ce2879c0f97dacccabd63
diff --git a/src/com/android/exchange/adapter/AbstractSyncParser.java b/src/com/android/exchange/adapter/AbstractSyncParser.java
index f8ddd1e..31fc562 100644
--- a/src/com/android/exchange/adapter/AbstractSyncParser.java
+++ b/src/com/android/exchange/adapter/AbstractSyncParser.java
@@ -68,6 +68,12 @@
         init(adapter);
     }
 
+    public AbstractSyncParser(final Parser p, final Context context, final ContentResolver resolver,
+        final Mailbox mailbox, final Account account) throws IOException {
+        super(p);
+        init(context, resolver, mailbox, account);
+    }
+
     private void init(final AbstractSyncAdapter adapter) {
         init(adapter.mContext, adapter.mContext.getContentResolver(), adapter.mMailbox,
                 adapter.mAccount);
diff --git a/src/com/android/exchange/adapter/EmailSyncParser.java b/src/com/android/exchange/adapter/EmailSyncParser.java
index f566276..fef6c31 100644
--- a/src/com/android/exchange/adapter/EmailSyncParser.java
+++ b/src/com/android/exchange/adapter/EmailSyncParser.java
@@ -102,6 +102,18 @@
         }
     }
 
+    public EmailSyncParser(final Parser parser, final Context context,
+            final ContentResolver resolver, final Mailbox mailbox, final Account account)
+                    throws IOException {
+        super(parser, context, resolver, mailbox, account);
+        mMailboxIdAsString = Long.toString(mMailbox.mId);
+        if (mAccount.mPolicyKey != 0) {
+            mPolicy = Policy.restorePolicyWithId(mContext, mAccount.mPolicyKey);
+        } else {
+            mPolicy = null;
+        }
+    }
+
     public boolean fetchNeeded() {
         return mFetchNeeded;
     }
@@ -110,7 +122,7 @@
         return mMessageUpdateStatus;
     }
 
-    public void addData (EmailContent.Message msg, int endingTag) throws IOException {
+    public void addData(EmailContent.Message msg, int endingTag) throws IOException {
         ArrayList<EmailContent.Attachment> atts = new ArrayList<EmailContent.Attachment>();
         boolean truncated = false;
 
diff --git a/src/com/android/exchange/adapter/SearchParser.java b/src/com/android/exchange/adapter/SearchParser.java
new file mode 100644
index 0000000..7a65370
--- /dev/null
+++ b/src/com/android/exchange/adapter/SearchParser.java
@@ -0,0 +1,143 @@
+package com.android.exchange.adapter;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.os.RemoteException;
+
+import com.android.emailcommon.Logging;
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.emailcommon.provider.EmailContent.Message;
+import com.android.emailcommon.utility.TextUtilities;
+import com.android.exchange.Eas;
+import com.android.mail.utils.LogUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+/**
+ * Parse the result of a Search command
+ */
+public class SearchParser extends Parser {
+    private static final String LOG_TAG = Logging.LOG_TAG;
+    private final Context mContext;
+    private final ContentResolver mContentResolver;
+    private final Mailbox mMailbox;
+    private final Account mAccount;
+    private final String mQuery;
+    private int mTotalResults;
+
+    public SearchParser(final Context context, final ContentResolver resolver,
+        final InputStream in, final Mailbox mailbox, final Account account,
+        String query)
+            throws IOException {
+        super(in);
+        mContext = context;
+        mContentResolver = resolver;
+        mMailbox = mailbox;
+        mAccount = account;
+        mQuery = query;
+    }
+
+    public int getTotalResults() {
+        return mTotalResults;
+    }
+
+    @Override
+    public boolean parse() throws IOException {
+        boolean res = false;
+        if (nextTag(START_DOCUMENT) != Tags.SEARCH_SEARCH) {
+            throw new IOException();
+        }
+        while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
+            if (tag == Tags.SEARCH_STATUS) {
+                String status = getValue();
+                if (Eas.USER_LOG) {
+                    LogUtils.d(Logging.LOG_TAG, "Search status: " + status);
+                }
+            } else if (tag == Tags.SEARCH_RESPONSE) {
+                parseResponse();
+            } else {
+                skipTag();
+            }
+        }
+        return res;
+    }
+
+    private boolean parseResponse() throws IOException {
+        boolean res = false;
+        while (nextTag(Tags.SEARCH_RESPONSE) != END) {
+            if (tag == Tags.SEARCH_STORE) {
+                parseStore();
+            } else {
+                skipTag();
+            }
+        }
+        return res;
+    }
+
+    private boolean parseStore() throws IOException {
+        EmailSyncParser parser = new EmailSyncParser(this, mContext, mContentResolver,
+                mMailbox, mAccount);
+        ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
+        boolean res = false;
+
+        while (nextTag(Tags.SEARCH_STORE) != END) {
+            if (tag == Tags.SEARCH_STATUS) {
+                getValue();
+            } else if (tag == Tags.SEARCH_TOTAL) {
+                mTotalResults = getValueInt();
+            } else if (tag == Tags.SEARCH_RESULT) {
+                parseResult(parser, ops);
+            } else {
+                skipTag();
+            }
+        }
+
+        try {
+            // FLAG: In EmailSyncParser.commit(), we have complicated logic to constrain the size
+            // of the batch, and fall back to one op at a time if that fails. We don't have any
+            // such logic here, but we probably should.
+            mContentResolver.applyBatch(EmailContent.AUTHORITY, ops);
+            LogUtils.d(Logging.LOG_TAG, "Saved %s search results", ops.size());
+        } catch (RemoteException e) {
+            LogUtils.d(Logging.LOG_TAG, "RemoteException while saving search results.");
+        } catch (OperationApplicationException e) {
+        }
+
+        return res;
+    }
+
+    private boolean parseResult(EmailSyncParser parser,
+            ArrayList<ContentProviderOperation> ops) throws IOException {
+        // Get an email sync parser for our incoming message data
+        boolean res = false;
+        Message msg = new Message();
+        while (nextTag(Tags.SEARCH_RESULT) != END) {
+            if (tag == Tags.SYNC_CLASS) {
+                getValue();
+            } else if (tag == Tags.SYNC_COLLECTION_ID) {
+                getValue();
+            } else if (tag == Tags.SEARCH_LONG_ID) {
+                msg.mProtocolSearchInfo = getValue();
+            } else if (tag == Tags.SEARCH_PROPERTIES) {
+                msg.mAccountKey = mAccount.mId;
+                msg.mMailboxKey = mMailbox.mId;
+                msg.mFlagLoaded = Message.FLAG_LOADED_COMPLETE;
+                parser.pushTag(tag);
+                parser.addData(msg, tag);
+                if (msg.mHtml != null) {
+                    msg.mHtml = TextUtilities.highlightTermsInHtml(msg.mHtml, mQuery);
+                }
+                msg.addSaveOps(ops);
+            } else {
+                skipTag();
+            }
+        }
+        return res;
+    }
+}
diff --git a/src/com/android/exchange/eas/EasSearch.java b/src/com/android/exchange/eas/EasSearch.java
new file mode 100644
index 0000000..34869bb
--- /dev/null
+++ b/src/com/android/exchange/eas/EasSearch.java
@@ -0,0 +1,165 @@
+package com.android.exchange.eas;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.SyncResult;
+
+import com.android.emailcommon.Logging;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.emailcommon.service.SearchParams;
+import com.android.exchange.CommandStatusException;
+import com.android.exchange.Eas;
+import com.android.exchange.EasResponse;
+import com.android.exchange.adapter.Serializer;
+import com.android.exchange.adapter.Tags;
+import com.android.exchange.adapter.SearchParser;
+import com.android.mail.providers.UIProvider;
+import com.android.mail.utils.LogUtils;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.entity.ByteArrayEntity;
+
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class EasSearch extends EasOperation {
+
+    public final static int RESULT_NO_MESSAGES = 0;
+    public final static int RESULT_OK = 1;
+    public final static int RESULT_EMPTY_RESPONSE = 2;
+
+    // The shortest search query we'll accept
+    // TODO Check with UX whether this is correct
+    private static final int MIN_QUERY_LENGTH = 3;
+    // The largest number of results we'll ask for per server request
+    private static final int MAX_SEARCH_RESULTS = 100;
+
+    final SearchParams mSearchParams;
+    final long mDestMailboxId;
+    int mTotalResults;
+
+    public EasSearch(final Context context, final long accountId, final SearchParams searchParams,
+        final long destMailboxId) {
+        super(context, accountId);
+        mSearchParams = searchParams;
+        mDestMailboxId = destMailboxId;
+    }
+
+    public int getTotalResults() {
+        return mTotalResults;
+    }
+
+    @Override
+    protected String getCommand() {
+        return "Search";
+    }
+
+    @Override
+    protected HttpEntity getRequestEntity() throws IOException {
+        // Sanity check for arguments
+        final int offset = mSearchParams.mOffset;
+        final int limit = mSearchParams.mLimit;
+        final String filter = mSearchParams.mFilter;
+        if (limit < 0 || limit > MAX_SEARCH_RESULTS || offset < 0) {
+            return null;
+        }
+        // TODO Should this be checked in UI?  Are there guidelines for minimums?
+        if (filter == null || filter.length() < MIN_QUERY_LENGTH) {
+            LogUtils.w(LOG_TAG, "filter too short");
+            return null;
+        }
+
+        int res = 0;
+        final Mailbox searchMailbox = Mailbox.restoreMailboxWithId(mContext, mDestMailboxId);
+        // Sanity check; account might have been deleted?
+        if (searchMailbox == null) {
+            LogUtils.i(LOG_TAG, "search mailbox ceased to exist");
+            return null;
+        }
+        final ContentValues statusValues = new ContentValues(2);
+        try {
+            // Set the status of this mailbox to indicate query
+            statusValues.put(Mailbox.UI_SYNC_STATUS, UIProvider.SyncStatus.LIVE_QUERY);
+            searchMailbox.update(mContext, statusValues);
+
+            final Serializer s = new Serializer();
+            s.start(Tags.SEARCH_SEARCH).start(Tags.SEARCH_STORE);
+            s.data(Tags.SEARCH_NAME, "Mailbox");
+            s.start(Tags.SEARCH_QUERY).start(Tags.SEARCH_AND);
+            s.data(Tags.SYNC_CLASS, "Email");
+
+            // If this isn't an inbox search, then include the collection id
+            final Mailbox inbox =
+                    Mailbox.restoreMailboxOfType(mContext, mAccount.mId, Mailbox.TYPE_INBOX);
+            if (inbox == null) {
+                LogUtils.i(LOG_TAG, "Inbox ceased to exist");
+                return null;
+            }
+            if (mSearchParams.mMailboxId != inbox.mId) {
+                s.data(Tags.SYNC_COLLECTION_ID, inbox.mServerId);
+            }
+            s.data(Tags.SEARCH_FREE_TEXT, filter);
+
+            // Add the date window if appropriate
+            if (mSearchParams.mStartDate != null) {
+                s.start(Tags.SEARCH_GREATER_THAN);
+                s.tag(Tags.EMAIL_DATE_RECEIVED);
+                s.data(Tags.SEARCH_VALUE, Eas.DATE_FORMAT.format(mSearchParams.mStartDate));
+                s.end(); // SEARCH_GREATER_THAN
+            }
+            if (mSearchParams.mEndDate != null) {
+                s.start(Tags.SEARCH_LESS_THAN);
+                s.tag(Tags.EMAIL_DATE_RECEIVED);
+                s.data(Tags.SEARCH_VALUE, Eas.DATE_FORMAT.format(mSearchParams.mEndDate));
+                s.end(); // SEARCH_LESS_THAN
+            }
+            s.end().end(); // SEARCH_AND, SEARCH_QUERY
+            s.start(Tags.SEARCH_OPTIONS);
+            if (offset == 0) {
+                s.tag(Tags.SEARCH_REBUILD_RESULTS);
+            }
+            if (mSearchParams.mIncludeChildren) {
+                s.tag(Tags.SEARCH_DEEP_TRAVERSAL);
+            }
+            // Range is sent in the form first-last (e.g. 0-9)
+            s.data(Tags.SEARCH_RANGE, offset + "-" + (offset + limit - 1));
+            s.start(Tags.BASE_BODY_PREFERENCE);
+            s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_HTML);
+            s.data(Tags.BASE_TRUNCATION_SIZE, "20000");
+            s.end();                    // BASE_BODY_PREFERENCE
+            s.end().end().end().done(); // SEARCH_OPTIONS, SEARCH_STORE, SEARCH_SEARCH
+            return makeEntity(s);
+        } catch (IOException e) {
+            LogUtils.d(LOG_TAG, e, "Search exception");
+        } finally {
+            // TODO: Handle error states
+            // Set the status of this mailbox to indicate query over
+            statusValues.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
+            statusValues.put(Mailbox.UI_SYNC_STATUS, UIProvider.SyncStatus.NO_SYNC);
+            searchMailbox.update(mContext, statusValues);
+        }
+        LogUtils.i(LOG_TAG, "end returning null");
+        return null;
+    }
+
+    @Override
+    protected int handleResponse(final EasResponse response)
+        throws IOException, CommandStatusException {
+        if (response.isEmpty()) {
+            return RESULT_EMPTY_RESPONSE;
+        }
+        final InputStream is = response.getInputStream();
+        try {
+            final Mailbox searchMailbox = Mailbox.restoreMailboxWithId(mContext, mDestMailboxId);
+            final SearchParser sp = new SearchParser(mContext, mContext.getContentResolver(),
+                    is, searchMailbox, mAccount, mSearchParams.mFilter);
+            sp.parse();
+            mTotalResults = sp.getTotalResults();
+        } finally {
+            is.close();
+        }
+        return RESULT_OK;
+    }
+}
diff --git a/src/com/android/exchange/service/EasService.java b/src/com/android/exchange/service/EasService.java
index b7c366d..3794423 100644
--- a/src/com/android/exchange/service/EasService.java
+++ b/src/com/android/exchange/service/EasService.java
@@ -39,6 +39,7 @@
 import com.android.exchange.eas.EasFolderSync;
 import com.android.exchange.eas.EasLoadAttachment;
 import com.android.exchange.eas.EasOperation;
+import com.android.exchange.eas.EasSearch;
 import com.android.mail.utils.LogUtils;
 
 import java.util.HashSet;
@@ -64,7 +65,7 @@
     private final PingSyncSynchronizer mSynchronizer;
 
     /**
-     * Implementation of the IEmailService interface.
+     * Implementation of the IMailService interface.
      * For the most part these calls should consist of creating the correct {@link EasOperation}
      * class and calling {@link #doOperation} with it.
      */
@@ -112,8 +113,10 @@
         @Override
         public int searchMessages(final long accountId, final SearchParams searchParams,
                 final long destMailboxId) {
-            LogUtils.d(TAG, "IEmailService.searchMessages");
-            return 0;
+            final EasSearch operation = new EasSearch(EasService.this, accountId, searchParams,
+                    destMailboxId);
+            doOperation(operation, "IEmailService.searchMessages");
+            return operation.getTotalResults();
         }
 
         @Override
diff --git a/src/com/android/exchange/service/EmailSyncAdapterService.java b/src/com/android/exchange/service/EmailSyncAdapterService.java
index eb17500..0e4bfed 100644
--- a/src/com/android/exchange/service/EmailSyncAdapterService.java
+++ b/src/com/android/exchange/service/EmailSyncAdapterService.java
@@ -60,12 +60,12 @@
 import com.android.exchange.R.drawable;
 import com.android.exchange.R.string;
 import com.android.exchange.adapter.PingParser;
-import com.android.exchange.adapter.Search;
 import com.android.exchange.eas.EasFolderSync;
 import com.android.exchange.eas.EasLoadAttachment;
 import com.android.exchange.eas.EasMoveItems;
 import com.android.exchange.eas.EasOperation;
 import com.android.exchange.eas.EasPing;
+import com.android.exchange.eas.EasSearch;
 import com.android.exchange.eas.EasSync;
 import com.android.mail.providers.UIProvider;
 import com.android.mail.utils.LogUtils;
@@ -460,8 +460,10 @@
         public int searchMessages(final long accountId, final SearchParams searchParams,
                 final long destMailboxId) {
             LogUtils.d(TAG, "IEmailService.searchMessages");
-            return Search.searchMessages(EmailSyncAdapterService.this, accountId, searchParams,
-                    destMailboxId);
+            final EasSearch operation = new EasSearch(EmailSyncAdapterService.this, accountId,
+                    searchParams, destMailboxId);
+            operation.performOperation();
+            return operation.getTotalResults();
             // TODO: may need an explicit callback to replace the one to IEmailServiceCallback.
         }