1/* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.exchange.adapter; 18 19import android.content.ContentProviderOperation; 20import android.content.ContentValues; 21import android.content.Context; 22import android.content.OperationApplicationException; 23import android.os.RemoteException; 24 25import com.android.emailcommon.Logging; 26import com.android.emailcommon.provider.Account; 27import com.android.emailcommon.provider.EmailContent; 28import com.android.emailcommon.provider.EmailContent.Message; 29import com.android.emailcommon.provider.Mailbox; 30import com.android.emailcommon.service.SearchParams; 31import com.android.emailcommon.utility.TextUtilities; 32import com.android.exchange.Eas; 33import com.android.exchange.EasResponse; 34import com.android.exchange.EasSyncService; 35import com.android.exchange.adapter.EmailSyncAdapter.EasEmailSyncParser; 36import com.android.mail.providers.UIProvider; 37import com.android.mail.utils.LogUtils; 38 39import org.apache.http.HttpStatus; 40 41import java.io.IOException; 42import java.io.InputStream; 43import java.util.ArrayList; 44 45/** 46 * Implementation of server-side search for EAS using the EmailService API 47 */ 48public class Search { 49 // The shortest search query we'll accept 50 // TODO Check with UX whether this is correct 51 private static final int MIN_QUERY_LENGTH = 3; 52 // The largest number of results we'll ask for per server request 53 private static final int MAX_SEARCH_RESULTS = 100; 54 55 public static int searchMessages(Context context, long accountId, SearchParams searchParams, 56 long destMailboxId) { 57 // Sanity check for arguments 58 final int offset = searchParams.mOffset; 59 final int limit = searchParams.mLimit; 60 final String filter = searchParams.mFilter; 61 if (limit < 0 || limit > MAX_SEARCH_RESULTS || offset < 0) return 0; 62 // TODO Should this be checked in UI? Are there guidelines for minimums? 63 if (filter == null || filter.length() < MIN_QUERY_LENGTH) return 0; 64 65 int res = 0; 66 final Account account = Account.restoreAccountWithId(context, accountId); 67 if (account == null) return res; 68 final EasSyncService svc = EasSyncService.setupServiceForAccount(context, account); 69 if (svc == null) return res; 70 final Mailbox searchMailbox = Mailbox.restoreMailboxWithId(context, destMailboxId); 71 // Sanity check; account might have been deleted? 72 if (searchMailbox == null) return res; 73 final ContentValues statusValues = new ContentValues(2); 74 try { 75 // Set the status of this mailbox to indicate query 76 statusValues.put(Mailbox.UI_SYNC_STATUS, UIProvider.SyncStatus.LIVE_QUERY); 77 searchMailbox.update(context, statusValues); 78 79 svc.mMailbox = searchMailbox; 80 svc.mAccount = account; 81 final Serializer s = new Serializer(); 82 s.start(Tags.SEARCH_SEARCH).start(Tags.SEARCH_STORE); 83 s.data(Tags.SEARCH_NAME, "Mailbox"); 84 s.start(Tags.SEARCH_QUERY).start(Tags.SEARCH_AND); 85 s.data(Tags.SYNC_CLASS, "Email"); 86 87 // If this isn't an inbox search, then include the collection id 88 final Mailbox inbox = 89 Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_INBOX); 90 if (inbox == null) return 0; 91 if (searchParams.mMailboxId != inbox.mId) { 92 s.data(Tags.SYNC_COLLECTION_ID, inbox.mServerId); 93 } 94 95 s.data(Tags.SEARCH_FREE_TEXT, filter); 96 97 // Add the date window if appropriate 98 if (searchParams.mStartDate != null) { 99 s.start(Tags.SEARCH_GREATER_THAN); 100 s.tag(Tags.EMAIL_DATE_RECEIVED); 101 s.data(Tags.SEARCH_VALUE, Eas.DATE_FORMAT.format(searchParams.mStartDate)); 102 s.end(); // SEARCH_GREATER_THAN 103 } 104 if (searchParams.mEndDate != null) { 105 s.start(Tags.SEARCH_LESS_THAN); 106 s.tag(Tags.EMAIL_DATE_RECEIVED); 107 s.data(Tags.SEARCH_VALUE, Eas.DATE_FORMAT.format(searchParams.mEndDate)); 108 s.end(); // SEARCH_LESS_THAN 109 } 110 s.end().end(); // SEARCH_AND, SEARCH_QUERY 111 s.start(Tags.SEARCH_OPTIONS); 112 if (offset == 0) { 113 s.tag(Tags.SEARCH_REBUILD_RESULTS); 114 } 115 if (searchParams.mIncludeChildren) { 116 s.tag(Tags.SEARCH_DEEP_TRAVERSAL); 117 } 118 // Range is sent in the form first-last (e.g. 0-9) 119 s.data(Tags.SEARCH_RANGE, offset + "-" + (offset + limit - 1)); 120 s.start(Tags.BASE_BODY_PREFERENCE); 121 s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_HTML); 122 s.data(Tags.BASE_TRUNCATION_SIZE, "20000"); 123 s.end(); // BASE_BODY_PREFERENCE 124 s.end().end().end().done(); // SEARCH_OPTIONS, SEARCH_STORE, SEARCH_SEARCH 125 final EasResponse resp = svc.sendHttpClientPost("Search", s.toByteArray()); 126 try { 127 final int code = resp.getStatus(); 128 if (code == HttpStatus.SC_OK) { 129 final InputStream is = resp.getInputStream(); 130 try { 131 final SearchParser sp = new SearchParser(is, svc, filter); 132 sp.parse(); 133 res = sp.getTotalResults(); 134 } finally { 135 is.close(); 136 } 137 } else { 138 svc.userLog("Search returned " + code); 139 } 140 } finally { 141 resp.close(); 142 } 143 } catch (IOException e) { 144 svc.userLog("Search exception " + e); 145 } finally { 146 // TODO: Handle error states 147 // Set the status of this mailbox to indicate query over 148 statusValues.put(Mailbox.SYNC_TIME, System.currentTimeMillis()); 149 statusValues.put(Mailbox.UI_SYNC_STATUS, UIProvider.SyncStatus.NO_SYNC); 150 searchMailbox.update(context, statusValues); 151 } 152 // Return the total count 153 return res; 154 } 155 156 /** 157 * Parse the result of a Search command 158 */ 159 static class SearchParser extends Parser { 160 private final EasSyncService mService; 161 private final String mQuery; 162 private int mTotalResults; 163 164 private SearchParser(InputStream in, EasSyncService service, String query) 165 throws IOException { 166 super(in); 167 mService = service; 168 mQuery = query; 169 } 170 171 protected int getTotalResults() { 172 return mTotalResults; 173 } 174 175 @Override 176 public boolean parse() throws IOException { 177 boolean res = false; 178 if (nextTag(START_DOCUMENT) != Tags.SEARCH_SEARCH) { 179 throw new IOException(); 180 } 181 while (nextTag(START_DOCUMENT) != END_DOCUMENT) { 182 if (tag == Tags.SEARCH_STATUS) { 183 String status = getValue(); 184 if (Eas.USER_LOG) { 185 LogUtils.d(Logging.LOG_TAG, "Search status: " + status); 186 } 187 } else if (tag == Tags.SEARCH_RESPONSE) { 188 parseResponse(); 189 } else { 190 skipTag(); 191 } 192 } 193 return res; 194 } 195 196 private boolean parseResponse() throws IOException { 197 boolean res = false; 198 while (nextTag(Tags.SEARCH_RESPONSE) != END) { 199 if (tag == Tags.SEARCH_STORE) { 200 parseStore(); 201 } else { 202 skipTag(); 203 } 204 } 205 return res; 206 } 207 208 private boolean parseStore() throws IOException { 209 EmailSyncAdapter adapter = new EmailSyncAdapter(mService); 210 EasEmailSyncParser parser = new EasEmailSyncParser(this, adapter); 211 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); 212 boolean res = false; 213 214 while (nextTag(Tags.SEARCH_STORE) != END) { 215 if (tag == Tags.SEARCH_STATUS) { 216 getValue(); 217 } else if (tag == Tags.SEARCH_TOTAL) { 218 mTotalResults = getValueInt(); 219 } else if (tag == Tags.SEARCH_RESULT) { 220 parseResult(parser, ops); 221 } else { 222 skipTag(); 223 } 224 } 225 226 try { 227 adapter.mContentResolver.applyBatch(EmailContent.AUTHORITY, ops); 228 if (Eas.USER_LOG) { 229 mService.userLog("Saved " + ops.size() + " search results"); 230 } 231 } catch (RemoteException e) { 232 LogUtils.d(Logging.LOG_TAG, "RemoteException while saving search results."); 233 } catch (OperationApplicationException e) { 234 } 235 236 return res; 237 } 238 239 private boolean parseResult(EasEmailSyncParser parser, 240 ArrayList<ContentProviderOperation> ops) throws IOException { 241 // Get an email sync parser for our incoming message data 242 boolean res = false; 243 Message msg = new Message(); 244 while (nextTag(Tags.SEARCH_RESULT) != END) { 245 if (tag == Tags.SYNC_CLASS) { 246 getValue(); 247 } else if (tag == Tags.SYNC_COLLECTION_ID) { 248 getValue(); 249 } else if (tag == Tags.SEARCH_LONG_ID) { 250 msg.mProtocolSearchInfo = getValue(); 251 } else if (tag == Tags.SEARCH_PROPERTIES) { 252 msg.mAccountKey = mService.mAccount.mId; 253 msg.mMailboxKey = mService.mMailbox.mId; 254 msg.mFlagLoaded = Message.FLAG_LOADED_COMPLETE; 255 parser.pushTag(tag); 256 parser.addData(msg, tag); 257 if (msg.mHtml != null) { 258 msg.mHtml = TextUtilities.highlightTermsInHtml(msg.mHtml, mQuery); 259 } 260 msg.addSaveOps(ops); 261 } else { 262 skipTag(); 263 } 264 } 265 return res; 266 } 267 } 268} 269