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.Context; 21import android.content.OperationApplicationException; 22import android.os.RemoteException; 23import android.util.Log; 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.EmailServiceStatus; 31import com.android.emailcommon.service.SearchParams; 32import com.android.emailcommon.utility.TextUtilities; 33import com.android.exchange.Eas; 34import com.android.exchange.EasResponse; 35import com.android.exchange.EasSyncService; 36import com.android.exchange.ExchangeService; 37import com.android.exchange.adapter.EmailSyncAdapter.EasEmailSyncParser; 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 int offset = searchParams.mOffset; 59 int limit = searchParams.mLimit; 60 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 Account account = Account.restoreAccountWithId(context, accountId); 67 if (account == null) return res; 68 EasSyncService svc = EasSyncService.setupServiceForAccount(context, account); 69 if (svc == null) return res; 70 try { 71 Mailbox searchMailbox = Mailbox.restoreMailboxWithId(context, destMailboxId); 72 // Sanity check; account might have been deleted? 73 if (searchMailbox == null) return res; 74 svc.mMailbox = searchMailbox; 75 svc.mAccount = account; 76 Serializer s = new Serializer(); 77 s.start(Tags.SEARCH_SEARCH).start(Tags.SEARCH_STORE); 78 s.data(Tags.SEARCH_NAME, "Mailbox"); 79 s.start(Tags.SEARCH_QUERY).start(Tags.SEARCH_AND); 80 s.data(Tags.SYNC_CLASS, "Email"); 81 82 // If this isn't an inbox search, then include the collection id 83 Mailbox inbox = Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_INBOX); 84 if (inbox == null) return 0; 85 if (searchParams.mMailboxId != inbox.mId) { 86 s.data(Tags.SYNC_COLLECTION_ID, inbox.mServerId); 87 } 88 89 s.data(Tags.SEARCH_FREE_TEXT, filter); 90 s.end().end(); // SEARCH_AND, SEARCH_QUERY 91 s.start(Tags.SEARCH_OPTIONS); 92 if (offset == 0) { 93 s.tag(Tags.SEARCH_REBUILD_RESULTS); 94 } 95 if (searchParams.mIncludeChildren) { 96 s.tag(Tags.SEARCH_DEEP_TRAVERSAL); 97 } 98 // Range is sent in the form first-last (e.g. 0-9) 99 s.data(Tags.SEARCH_RANGE, offset + "-" + (offset + limit - 1)); 100 s.start(Tags.BASE_BODY_PREFERENCE); 101 s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_HTML); 102 s.data(Tags.BASE_TRUNCATION_SIZE, "20000"); 103 s.end(); // BASE_BODY_PREFERENCE 104 s.end().end().end().done(); // SEARCH_OPTIONS, SEARCH_STORE, SEARCH_SEARCH 105 EasResponse resp = svc.sendHttpClientPost("Search", s.toByteArray()); 106 try { 107 int code = resp.getStatus(); 108 if (code == HttpStatus.SC_OK) { 109 InputStream is = resp.getInputStream(); 110 try { 111 SearchParser sp = new SearchParser(is, svc, filter); 112 sp.parse(); 113 res = sp.getTotalResults(); 114 } finally { 115 is.close(); 116 } 117 } else { 118 svc.userLog("Search returned " + code); 119 } 120 } finally { 121 resp.close(); 122 } 123 } catch (IOException e) { 124 svc.userLog("Search exception " + e); 125 } finally { 126 try { 127 ExchangeService.callback().syncMailboxStatus(destMailboxId, 128 EmailServiceStatus.SUCCESS, 100); 129 } catch (RemoteException e) { 130 } 131 } 132 // Return the total count 133 return res; 134 } 135 136 /** 137 * Parse the result of a Search command 138 */ 139 static class SearchParser extends Parser { 140 private final EasSyncService mService; 141 private final String mQuery; 142 private int mTotalResults; 143 144 private SearchParser(InputStream in, EasSyncService service, String query) 145 throws IOException { 146 super(in); 147 mService = service; 148 mQuery = query; 149 } 150 151 protected int getTotalResults() { 152 return mTotalResults; 153 } 154 155 @Override 156 public boolean parse() throws IOException { 157 boolean res = false; 158 if (nextTag(START_DOCUMENT) != Tags.SEARCH_SEARCH) { 159 throw new IOException(); 160 } 161 while (nextTag(START_DOCUMENT) != END_DOCUMENT) { 162 if (tag == Tags.SEARCH_STATUS) { 163 String status = getValue(); 164 if (Eas.USER_LOG) { 165 Log.d(Logging.LOG_TAG, "Search status: " + status); 166 } 167 } else if (tag == Tags.SEARCH_RESPONSE) { 168 parseResponse(); 169 } else { 170 skipTag(); 171 } 172 } 173 return res; 174 } 175 176 private boolean parseResponse() throws IOException { 177 boolean res = false; 178 while (nextTag(Tags.SEARCH_RESPONSE) != END) { 179 if (tag == Tags.SEARCH_STORE) { 180 parseStore(); 181 } else { 182 skipTag(); 183 } 184 } 185 return res; 186 } 187 188 private boolean parseStore() throws IOException { 189 EmailSyncAdapter adapter = new EmailSyncAdapter(mService); 190 EasEmailSyncParser parser = adapter.new EasEmailSyncParser(this, adapter); 191 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); 192 boolean res = false; 193 194 while (nextTag(Tags.SEARCH_STORE) != END) { 195 if (tag == Tags.SEARCH_STATUS) { 196 String status = getValue(); 197 } else if (tag == Tags.SEARCH_TOTAL) { 198 mTotalResults = getValueInt(); 199 } else if (tag == Tags.SEARCH_RESULT) { 200 parseResult(parser, ops); 201 } else { 202 skipTag(); 203 } 204 } 205 206 try { 207 adapter.mContentResolver.applyBatch(EmailContent.AUTHORITY, ops); 208 if (Eas.USER_LOG) { 209 mService.userLog("Saved " + ops.size() + " search results"); 210 } 211 } catch (RemoteException e) { 212 Log.d(Logging.LOG_TAG, "RemoteException while saving search results."); 213 } catch (OperationApplicationException e) { 214 } 215 216 return res; 217 } 218 219 private boolean parseResult(EasEmailSyncParser parser, 220 ArrayList<ContentProviderOperation> ops) throws IOException { 221 // Get an email sync parser for our incoming message data 222 boolean res = false; 223 Message msg = new Message(); 224 while (nextTag(Tags.SEARCH_RESULT) != END) { 225 if (tag == Tags.SYNC_CLASS) { 226 getValue(); 227 } else if (tag == Tags.SYNC_COLLECTION_ID) { 228 getValue(); 229 } else if (tag == Tags.SEARCH_LONG_ID) { 230 msg.mProtocolSearchInfo = getValue(); 231 } else if (tag == Tags.SEARCH_PROPERTIES) { 232 msg.mAccountKey = mService.mAccount.mId; 233 msg.mMailboxKey = mService.mMailbox.mId; 234 msg.mFlagLoaded = Message.FLAG_LOADED_COMPLETE; 235 parser.pushTag(tag); 236 parser.addData(msg, tag); 237 if (msg.mHtml != null) { 238 msg.mHtml = TextUtilities.highlightTermsInHtml(msg.mHtml, mQuery); 239 } 240 msg.addSaveOps(ops); 241 } else { 242 skipTag(); 243 } 244 } 245 return res; 246 } 247 } 248} 249