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