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;
24import android.util.Log;
25
26import com.android.emailcommon.Logging;
27import com.android.emailcommon.provider.Account;
28import com.android.emailcommon.provider.EmailContent;
29import com.android.emailcommon.provider.EmailContent.Message;
30import com.android.emailcommon.provider.Mailbox;
31import com.android.emailcommon.service.EmailServiceStatus;
32import com.android.emailcommon.service.SearchParams;
33import com.android.emailcommon.utility.TextUtilities;
34import com.android.exchange.Eas;
35import com.android.exchange.EasResponse;
36import com.android.exchange.EasSyncService;
37import com.android.exchange.ExchangeService;
38import com.android.exchange.adapter.EmailSyncAdapter.EasEmailSyncParser;
39import com.android.mail.providers.UIProvider;
40
41import org.apache.http.HttpStatus;
42
43import java.io.IOException;
44import java.io.InputStream;
45import java.util.ArrayList;
46
47/**
48 * Implementation of server-side search for EAS using the EmailService API
49 */
50public 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
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.LIVE_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
155        private SearchParser(InputStream in, EasSyncService service, String query)
156                throws IOException {
157            super(in);
158            mService = service;
159            mQuery = query;
160        }
161
162        protected int getTotalResults() {
163            return mTotalResults;
164        }
165
166        @Override
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
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
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
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