Refactor EAS search into a single class

* Time to start uncluttering EasSyncService
* Add support for non-global (i.e. per mailbox) search
* searchMessages() now returns the total number of matches

Change-Id: I2491891f2b3035a66ca5340ac5ff14cb3617da43
diff --git a/src/com/android/exchange/Eas.java b/src/com/android/exchange/Eas.java
index ba3770b..9789f43 100644
--- a/src/com/android/exchange/Eas.java
+++ b/src/com/android/exchange/Eas.java
@@ -17,11 +17,11 @@
 
 package com.android.exchange;
 
+import android.util.Log;
+
 import com.android.emailcommon.service.EmailServiceProxy;
 import com.android.emailcommon.service.SyncWindow;
 
-import android.util.Log;
-
 /**
  * Constants used throughout the EAS implementation are stored here.
  *
@@ -36,7 +36,7 @@
     public static boolean PARSER_LOG = false;   // DO NOT CHECK IN WITH THIS SET TO TRUE
     public static boolean FILE_LOG = false;     // DO NOT CHECK IN WITH THIS SET TO TRUE
 
-    public static final String CLIENT_VERSION = "EAS-1.2";
+    public static final String CLIENT_VERSION = "EAS-1.3";
     public static final String ACCOUNT_MAILBOX_PREFIX = "__eas";
 
     // Define our default protocol version as 2.5 (Exchange 2003)
diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java
index 6feb47f..c213a51 100644
--- a/src/com/android/exchange/EasSyncService.java
+++ b/src/com/android/exchange/EasSyncService.java
@@ -51,7 +51,6 @@
 import com.android.emailcommon.service.EmailServiceConstants;
 import com.android.emailcommon.service.EmailServiceProxy;
 import com.android.emailcommon.service.EmailServiceStatus;
-import com.android.emailcommon.service.SearchParams;
 import com.android.emailcommon.utility.EmailClientConnectionManager;
 import com.android.emailcommon.utility.Utility;
 import com.android.exchange.CommandStatusException.CommandStatus;
@@ -69,7 +68,6 @@
 import com.android.exchange.adapter.Parser.EmptyStreamException;
 import com.android.exchange.adapter.PingParser;
 import com.android.exchange.adapter.ProvisionParser;
-import com.android.exchange.adapter.SearchParser;
 import com.android.exchange.adapter.Serializer;
 import com.android.exchange.adapter.Tags;
 import com.android.exchange.provider.GalResult;
@@ -194,12 +192,6 @@
     static private final String USER_AGENT = DEVICE_TYPE + '/' + Build.VERSION.RELEASE + '-' +
         Eas.CLIENT_VERSION;
 
-    // The shortest search query we'll accept
-    // TODO Check with UX whether this is correct
-    static private final int MIN_QUERY_LENGTH = 3;
-    // The largest number of results we'll ask for per server request
-    static private final int MAX_SEARCH_RESULTS = 100;
-
     // Reasonable default
     public String mProtocolVersion = Eas.DEFAULT_PROTOCOL_VERSION;
     public Double mProtocolVersionDouble;
@@ -434,7 +426,7 @@
      * @param account the account
      * @return the service, or null if the account is on hold or hasn't been initialized
      */
-    private static EasSyncService setupServiceForAccount(Context context, Account account) {
+    public static EasSyncService setupServiceForAccount(Context context, Account account) {
         // Just return null if we're on security hold
         if ((account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) {
             return null;
@@ -467,78 +459,6 @@
         return svc;
     }
 
-    public static int searchMessages(Context context, long accountId, SearchParams searchParams,
-            long destMailboxId) {
-        // Sanity check for arguments
-        int offset = searchParams.mOffset;
-        int limit = searchParams.mLimit;
-        String filter = searchParams.mFilter;
-        if (limit < 0 || limit > MAX_SEARCH_RESULTS || offset < 0) return 0;
-        // TODO Should this be checked in UI?  Are there guidelines for minimums?
-        if (filter == null || filter.length() < MIN_QUERY_LENGTH) return 0;
-
-        int res = 0;
-        Account account = Account.restoreAccountWithId(context, accountId);
-        if (account == null) return res;
-        EasSyncService svc = setupServiceForAccount(context, account);
-        if (svc == null) return res;
-        try {
-            Mailbox searchMailbox = Mailbox.restoreMailboxWithId(context, destMailboxId);
-            // Sanity check; account might have been deleted?
-            if (searchMailbox == null) return res;
-            svc.mMailbox = searchMailbox;
-            svc.mAccount = account;
-            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");
-            s.data(Tags.SEARCH_FREE_TEXT, filter);
-            s.end().end();              // SEARCH_AND, SEARCH_QUERY
-            s.start(Tags.SEARCH_OPTIONS);
-            if (offset == 0) {
-                s.tag(Tags.SEARCH_REBUILD_RESULTS);
-            }
-            if (searchParams.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
-            EasResponse resp = svc.sendHttpClientPost("Search", s.toByteArray());
-            try {
-                int code = resp.getStatus();
-                if (code == HttpStatus.SC_OK) {
-                    InputStream is = resp.getInputStream();
-                    try {
-                        new SearchParser(is, svc, filter).parse();
-                    } finally {
-                        is.close();
-                    }
-                } else {
-                    svc.userLog("Search returned " + code);
-                }
-            } finally {
-                resp.close();
-            }
-        } catch (IOException e) {
-            svc.userLog("Search exception " + e);
-        } finally {
-            try {
-                ExchangeService.callback().syncMailboxStatus(destMailboxId,
-                        EmailServiceStatus.SUCCESS, 100);
-            } catch (RemoteException e) {
-            }
-
-        }
-        // TODO Capture and return the correct value
-        return res;
-    }
-
     @Override
     public Bundle validateAccount(HostAuth hostAuth,  Context context) {
         Bundle bundle = new Bundle();
diff --git a/src/com/android/exchange/ExchangeService.java b/src/com/android/exchange/ExchangeService.java
index 9a8794b..dcec894 100644
--- a/src/com/android/exchange/ExchangeService.java
+++ b/src/com/android/exchange/ExchangeService.java
@@ -42,6 +42,7 @@
 import com.android.emailcommon.utility.Utility;
 import com.android.exchange.adapter.CalendarSyncAdapter;
 import com.android.exchange.adapter.ContactsSyncAdapter;
+import com.android.exchange.adapter.Search;
 import com.android.exchange.provider.MailboxUtilities;
 import com.android.exchange.utility.FileLogger;
 
@@ -486,7 +487,7 @@
         public int searchMessages(long accountId, SearchParams searchParams, long destMailboxId) {
             ExchangeService exchangeService = INSTANCE;
             if (exchangeService == null) return 0;
-            return EasSyncService.searchMessages(exchangeService, accountId, searchParams,
+            return Search.searchMessages(exchangeService, accountId, searchParams,
                     destMailboxId);
         }
     };
diff --git a/src/com/android/exchange/adapter/Search.java b/src/com/android/exchange/adapter/Search.java
new file mode 100644
index 0000000..011ab58
--- /dev/null
+++ b/src/com/android/exchange/adapter/Search.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2011 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.adapter;
+
+import android.content.ContentProviderOperation;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.emailcommon.Logging;
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent;
+import com.android.emailcommon.provider.EmailContent.Message;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.emailcommon.service.EmailServiceStatus;
+import com.android.emailcommon.service.SearchParams;
+import com.android.emailcommon.utility.TextUtilities;
+import com.android.exchange.Eas;
+import com.android.exchange.EasResponse;
+import com.android.exchange.EasSyncService;
+import com.android.exchange.ExchangeService;
+import com.android.exchange.adapter.EmailSyncAdapter.EasEmailSyncParser;
+
+import org.apache.http.HttpStatus;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+/**
+ * Implementation of server-side search for EAS using the EmailService API
+ */
+public class Search {
+    // 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;
+
+    public static int searchMessages(Context context, long accountId, SearchParams searchParams,
+            long destMailboxId) {
+        // Sanity check for arguments
+        int offset = searchParams.mOffset;
+        int limit = searchParams.mLimit;
+        String filter = searchParams.mFilter;
+        if (limit < 0 || limit > MAX_SEARCH_RESULTS || offset < 0) return 0;
+        // TODO Should this be checked in UI?  Are there guidelines for minimums?
+        if (filter == null || filter.length() < MIN_QUERY_LENGTH) return 0;
+
+        int res = 0;
+        Account account = Account.restoreAccountWithId(context, accountId);
+        if (account == null) return res;
+        EasSyncService svc = EasSyncService.setupServiceForAccount(context, account);
+        if (svc == null) return res;
+        try {
+            Mailbox searchMailbox = Mailbox.restoreMailboxWithId(context, destMailboxId);
+            // Sanity check; account might have been deleted?
+            if (searchMailbox == null) return res;
+            svc.mMailbox = searchMailbox;
+            svc.mAccount = account;
+            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
+            Mailbox inbox = Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_INBOX);
+            if (inbox == null) return 0;
+            if (searchParams.mMailboxId != inbox.mId) {
+                s.data(Tags.SYNC_COLLECTION_ID, inbox.mServerId);
+            }
+
+            s.data(Tags.SEARCH_FREE_TEXT, filter);
+            s.end().end();              // SEARCH_AND, SEARCH_QUERY
+            s.start(Tags.SEARCH_OPTIONS);
+            if (offset == 0) {
+                s.tag(Tags.SEARCH_REBUILD_RESULTS);
+            }
+            if (searchParams.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
+            EasResponse resp = svc.sendHttpClientPost("Search", s.toByteArray());
+            try {
+                int code = resp.getStatus();
+                if (code == HttpStatus.SC_OK) {
+                    InputStream is = resp.getInputStream();
+                    try {
+                        SearchParser sp = new SearchParser(is, svc, filter);
+                        sp.parse();
+                        res = sp.getTotalResults();
+                    } finally {
+                        is.close();
+                    }
+                } else {
+                    svc.userLog("Search returned " + code);
+                }
+            } finally {
+                resp.close();
+            }
+        } catch (IOException e) {
+            svc.userLog("Search exception " + e);
+        } finally {
+            try {
+                ExchangeService.callback().syncMailboxStatus(destMailboxId,
+                        EmailServiceStatus.SUCCESS, 100);
+            } catch (RemoteException e) {
+            }
+        }
+        // Return the total count
+        return res;
+    }
+
+    /**
+     * Parse the result of a Search command
+     */
+    static class SearchParser extends Parser {
+        private final EasSyncService mService;
+        private final String mQuery;
+        private int mTotalResults;
+
+        private SearchParser(InputStream in, EasSyncService service, String query)
+                throws IOException {
+            super(in);
+            mService = service;
+            mQuery = query;
+        }
+
+        protected 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.DEBUG) {
+                        Log.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 {
+            EmailSyncAdapter adapter = new EmailSyncAdapter(mService);
+            EasEmailSyncParser parser = adapter.new EasEmailSyncParser(this, adapter);
+            ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
+            boolean res = false;
+
+            while (nextTag(Tags.SEARCH_STORE) != END) {
+                if (tag == Tags.SEARCH_STATUS) {
+                    String status = getValue();
+                    if (Eas.DEBUG) {
+                        Log.d(Logging.LOG_TAG, "Store status: " + status);
+                    }
+                } else if (tag == Tags.SEARCH_TOTAL) {
+                    mTotalResults = getValueInt();
+                } else if (tag == Tags.SEARCH_RESULT) {
+                    parseResult(parser, ops);
+                } else {
+                    skipTag();
+                }
+            }
+
+            try {
+                adapter.mContentResolver.applyBatch(EmailContent.AUTHORITY, ops);
+                if (Eas.DEBUG) {
+                    mService.userLog("Saved " + ops.size() + " search results");
+                }
+            } catch (RemoteException e) {
+                Log.d(Logging.LOG_TAG, "RemoteException while saving search results.");
+            } catch (OperationApplicationException e) {
+            }
+
+            return res;
+        }
+
+        private boolean parseResult(EasEmailSyncParser 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 = mService.mAccount.mId;
+                    msg.mMailboxKey = mService.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/adapter/SearchParser.java b/src/com/android/exchange/adapter/SearchParser.java
deleted file mode 100644
index 833f346..0000000
--- a/src/com/android/exchange/adapter/SearchParser.java
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * Copyright (C) 2011 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.adapter;
-
-import android.content.ContentProviderOperation;
-import android.content.OperationApplicationException;
-import android.os.RemoteException;
-import android.util.Log;
-
-import com.android.emailcommon.Logging;
-import com.android.emailcommon.provider.EmailContent;
-import com.android.emailcommon.provider.EmailContent.Message;
-import com.android.emailcommon.utility.TextUtilities;
-import com.android.exchange.EasSyncService;
-import com.android.exchange.adapter.EmailSyncAdapter.EasEmailSyncParser;
-
-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 final EasSyncService mService;
-    private final String mQuery;
-
-    public SearchParser(InputStream in, EasSyncService service, String query) throws IOException {
-        super(in);
-        mService = service;
-        mQuery = query;
-    }
-
-    @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) {
-                Log.d(Logging.LOG_TAG, "Search status: " + getValue());
-            } else if (tag == Tags.SEARCH_RESPONSE) {
-                parseResponse();
-            } else {
-                skipTag();
-            }
-        }
-        return res;
-    }
-
-    public boolean parseResponse() throws IOException {
-        boolean res = false;
-        while (nextTag(Tags.SEARCH_RESPONSE) != END) {
-            if (tag == Tags.SEARCH_STORE) {
-                parseStore();
-            } else {
-                skipTag();
-            }
-        }
-        return res;
-    }
-
-    public boolean parseStore() throws IOException {
-        EmailSyncAdapter adapter = new EmailSyncAdapter(mService);
-        EasEmailSyncParser parser = adapter.new EasEmailSyncParser(this, adapter);
-        ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
-        boolean res = false;
-
-        while (nextTag(Tags.SEARCH_STORE) != END) {
-            if (tag == Tags.SEARCH_STATUS) {
-                Log.d(Logging.LOG_TAG, "Store status: " + getValue());
-            } else if (tag == Tags.SEARCH_RESULT) {
-                parseResult(parser, ops);
-            } else {
-                skipTag();
-            }
-        }
-
-        try {
-            adapter.mContentResolver.applyBatch(EmailContent.AUTHORITY, ops);
-            mService.userLog("Saved " + ops.size() + " search results");
-        } catch (RemoteException e) {
-            Log.d(Logging.LOG_TAG, "RemoteException while saving search results.");
-        } catch (OperationApplicationException e) {
-        }
-
-        return res;
-    }
-
-    public boolean parseResult(EasEmailSyncParser 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) {
-                Log.d(Logging.LOG_TAG, "Result class: " + getValue());
-            } else if (tag == Tags.SYNC_COLLECTION_ID) {
-                Log.d(Logging.LOG_TAG, "Result collectionId: " + getValue());
-            } else if (tag == Tags.SEARCH_LONG_ID) {
-                msg.mProtocolSearchInfo = getValue();
-            } else if (tag == Tags.SEARCH_PROPERTIES) {
-                msg.mAccountKey = mService.mAccount.mId;
-                msg.mMailboxKey = mService.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;
-    }
-}
-