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 41aa48c..0b0ef0c 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.
}