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;
- }
-}
-