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