Home | History | Annotate | Download | only in providers
      1 /*******************************************************************************
      2  *      Copyright (C) 2012 Google Inc.
      3  *      Licensed to The Android Open Source Project.
      4  *
      5  *      Licensed under the Apache License, Version 2.0 (the "License");
      6  *      you may not use this file except in compliance with the License.
      7  *      You may obtain a copy of the License at
      8  *
      9  *           http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  *      Unless required by applicable law or agreed to in writing, software
     12  *      distributed under the License is distributed on an "AS IS" BASIS,
     13  *      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  *      See the License for the specific language governing permissions and
     15  *      limitations under the License.
     16  *******************************************************************************/
     17 
     18 package com.android.mail.providers;
     19 
     20 import android.database.Cursor;
     21 import android.database.MergeCursor;
     22 import android.net.Uri;
     23 import android.provider.BaseColumns;
     24 import android.provider.ContactsContract;
     25 import android.app.SearchManager;
     26 import android.content.ContentResolver;
     27 import android.content.Context;
     28 import android.text.TextUtils;
     29 
     30 import com.android.mail.R;
     31 import com.android.mail.utils.MatrixCursorWithCachedColumns;
     32 
     33 import java.util.ArrayList;
     34 
     35 /**
     36  * Simple extension / instantiation of SearchRecentSuggestionsProvider, independent
     37  * of mail account or account capabilities.  Offers suggestions from historical searches
     38  * and contact email addresses on the device. The authority fro for this provider is obtained
     39  * through the MailAppProvider as follows:
     40  * final String AUTHORITY = MailAppProvider.getInstance().getSuggestionAuthority()
     41  * It needs to be done after the MailAppProvider is constructed.
     42  */
     43 public class SuggestionsProvider extends SearchRecentSuggestionsProvider {
     44     /**
     45      * Mode used in the constructor of SuggestionsProvider.
     46      */
     47     public final static int MODE = DATABASE_MODE_QUERIES;
     48     /**
     49      * Columns over the contacts database that we return in the {@link ContactsCursor}.
     50      */
     51     private static final String[] CONTACTS_COLUMNS = new String[] {
     52             BaseColumns._ID,
     53             SearchManager.SUGGEST_COLUMN_TEXT_1, SearchManager.SUGGEST_COLUMN_QUERY,
     54             SearchManager.SUGGEST_COLUMN_ICON_1
     55     };
     56     private ArrayList<String> mFullQueryTerms;
     57     /** Used for synchronization */
     58     private final Object mTermsLock = new Object();
     59     private final static String[] sContract = new String[] {
     60             ContactsContract.CommonDataKinds.Email.DISPLAY_NAME,
     61             ContactsContract.CommonDataKinds.Email.DATA
     62     };
     63     /**
     64      * Minimum length of query before we start showing contacts suggestions.
     65      */
     66     static private final int MIN_QUERY_LENGTH_FOR_CONTACTS = 2;
     67 
     68     public SuggestionsProvider() {
     69         super();
     70     }
     71 
     72     @Override
     73     public boolean onCreate() {
     74         final String authority = getContext().getString(R.string.suggestions_authority);
     75         setupSuggestions(authority, MODE);
     76         super.onCreate();
     77         return true;
     78     }
     79 
     80     @Override
     81     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
     82             String sortOrder) {
     83         String query = selectionArgs[0];
     84         MergeCursor mergeCursor = null;
     85 
     86         synchronized (mTermsLock) {
     87             mFullQueryTerms = null;
     88             super.setFullQueryTerms(mFullQueryTerms);
     89         }
     90         // Get the custom suggestions for email which are from, to, etc.
     91         if (query != null) {
     92             // Tokenize the query.
     93             String[] tokens = TextUtils.split(query,
     94                     SearchRecentSuggestionsProvider.QUERY_TOKEN_SEPARATOR);
     95             // There are multiple tokens, so query on the last token only.
     96             if (tokens != null && tokens.length > 1) {
     97                 query = tokens[tokens.length - 1];
     98                 // Leave off the last token since we are auto completing on it.
     99                 synchronized (mTermsLock) {
    100                     mFullQueryTerms = new ArrayList<String>();
    101                     for (int i = 0, size = tokens.length - 1; i < size; i++) {
    102                         mFullQueryTerms.add(tokens[i]);
    103                     }
    104                     super.setFullQueryTerms(mFullQueryTerms);
    105                 }
    106             } else {
    107                 // Strip excess whitespace.
    108                 query = query.trim();
    109             }
    110             ArrayList<Cursor> cursors = new ArrayList<Cursor>();
    111             // Pass query; at this point it is either the last term OR the
    112             // only term.
    113             cursors.add(super.query(uri, projection, selection, new String[] { query }, sortOrder));
    114 
    115             if (query.length() >= MIN_QUERY_LENGTH_FOR_CONTACTS) {
    116                 cursors.add(new ContactsCursor().query(query));
    117             }
    118             mergeCursor = new MergeCursor(cursors.toArray(new Cursor[cursors.size()]));
    119         }
    120         return mergeCursor;
    121     }
    122 
    123     /**
    124      * Utility class to return a cursor over the contacts database
    125      */
    126     private final class ContactsCursor extends MatrixCursorWithCachedColumns {
    127         private final Context mContext;
    128         public ContactsCursor() {
    129             super(CONTACTS_COLUMNS);
    130             mContext = getContext();
    131         }
    132 
    133         /**
    134          * Searches over the contacts cursor with the specified query as the starting characters to
    135          * match.
    136          * @param query
    137          * @return a cursor over the contacts database with the contacts matching the query.
    138          */
    139         public ContactsCursor query(String query) {
    140             final Uri contactsUri = Uri.withAppendedPath(
    141                     ContactsContract.CommonDataKinds.Email.CONTENT_FILTER_URI, Uri.encode(query));
    142             final Cursor cursor = mContext.getContentResolver().query(
    143                     contactsUri, sContract, null, null, null);
    144             // We don't want to show a contact icon here. Leaving the SEARCH_ICON_1 field
    145             // empty causes inconsistent behavior because the cursor is merged with the
    146             // historical suggestions, which have an icon.  The solution is to show an empty icon
    147             // instead.
    148             final String emptyIcon = ContentResolver.SCHEME_ANDROID_RESOURCE + "://"
    149                     + mContext.getPackageName() + "/" + R.drawable.empty;
    150             if (cursor != null) {
    151                 final int nameIndex = cursor
    152                         .getColumnIndex(ContactsContract.CommonDataKinds.Email.DISPLAY_NAME);
    153                 final int addressIndex = cursor
    154                         .getColumnIndex(ContactsContract.CommonDataKinds.Email.DATA);
    155                 String match;
    156                 while (cursor.moveToNext()) {
    157                     match = cursor.getString(nameIndex);
    158                     match = !TextUtils.isEmpty(match) ? match : cursor.getString(addressIndex);
    159                     // The order of fields is:
    160                     // _ID, SUGGEST_COLUMN_TEXT_1, SUGGEST_COLUMN_QUERY, SUGGEST_COLUMN_ICON_1
    161                     addRow(new Object[] {0, match, createQuery(match), emptyIcon});
    162                 }
    163                 cursor.close();
    164             }
    165             return this;
    166         }
    167     }
    168 
    169     private String createQuery(String inMatch) {
    170         final StringBuilder query = new StringBuilder();
    171         if (mFullQueryTerms != null) {
    172             synchronized (mTermsLock) {
    173                 for (int i = 0, size = mFullQueryTerms.size(); i < size; i++) {
    174                     query.append(mFullQueryTerms.get(i)).append(QUERY_TOKEN_SEPARATOR);
    175                 }
    176             }
    177         }
    178         // Append the match as well.
    179         query.append(inMatch);
    180         // Example:
    181         // Search terms in the searchbox are : "pdf test*"
    182         // Contacts database contains: test (at) tester.com, test (at) other.com
    183         // If the user taps "test (at) tester.com", the query passed with
    184         // ACTION_SEARCH is:
    185         // "pdf test (at) tester.com"
    186         // If the user taps "test (at) other.com", the query passed with
    187         // ACTION_SEARCH is:
    188         // "pdf test (at) other.com"
    189         return query.toString();
    190     }
    191 }