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