Home | History | Annotate | Download | only in adapter
      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 
     25 import com.android.emailcommon.Logging;
     26 import com.android.emailcommon.provider.Account;
     27 import com.android.emailcommon.provider.EmailContent;
     28 import com.android.emailcommon.provider.EmailContent.Message;
     29 import com.android.emailcommon.provider.Mailbox;
     30 import com.android.emailcommon.service.SearchParams;
     31 import com.android.emailcommon.utility.TextUtilities;
     32 import com.android.exchange.Eas;
     33 import com.android.exchange.EasResponse;
     34 import com.android.exchange.EasSyncService;
     35 import com.android.exchange.adapter.EmailSyncAdapter.EasEmailSyncParser;
     36 import com.android.mail.providers.UIProvider;
     37 import com.android.mail.utils.LogUtils;
     38 
     39 import org.apache.http.HttpStatus;
     40 
     41 import java.io.IOException;
     42 import java.io.InputStream;
     43 import java.util.ArrayList;
     44 
     45 /**
     46  * Implementation of server-side search for EAS using the EmailService API
     47  */
     48 public 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