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.
39  */
40 public class SuggestionsProvider extends SearchRecentSuggestionsProvider {
41     /**
42      * Columns over the contacts database that we return in the {@link ContactsCursor}.
43      */
44     private static final String[] CONTACTS_COLUMNS = new String[] {
45             BaseColumns._ID,
46             SearchManager.SUGGEST_COLUMN_TEXT_1, SearchManager.SUGGEST_COLUMN_QUERY,
47             SearchManager.SUGGEST_COLUMN_ICON_1
48     };
49     private ArrayList<String> mFullQueryTerms;
50     /** Used for synchronization */
51     private final Object mTermsLock = new Object();
52     private final static String[] sContract = new String[] {
53             ContactsContract.CommonDataKinds.Email.DISPLAY_NAME,
54             ContactsContract.CommonDataKinds.Email.DATA
55     };
56     /**
57      * Minimum length of query before we start showing contacts suggestions.
58      */
59     static private final int MIN_QUERY_LENGTH_FOR_CONTACTS = 2;
60 
SuggestionsProvider(Context context)61     public SuggestionsProvider(Context context) {
62         super(context);
63     }
64 
65     @Override
query(String query)66     public Cursor query(String query) {
67         Cursor mergeCursor = null;
68 
69         synchronized (mTermsLock) {
70             mFullQueryTerms = null;
71             super.setFullQueryTerms(mFullQueryTerms);
72         }
73         // Get the custom suggestions for email which are from, to, etc.
74         if (query != null) {
75             // Tokenize the query.
76             String[] tokens = TextUtils.split(query,
77                     SearchRecentSuggestionsProvider.QUERY_TOKEN_SEPARATOR);
78             // There are multiple tokens, so query on the last token only.
79             if (tokens != null && tokens.length > 1) {
80                 query = tokens[tokens.length - 1];
81                 // Leave off the last token since we are auto completing on it.
82                 synchronized (mTermsLock) {
83                     mFullQueryTerms = new ArrayList<String>();
84                     for (int i = 0, size = tokens.length - 1; i < size; i++) {
85                         mFullQueryTerms.add(tokens[i]);
86                     }
87                     super.setFullQueryTerms(mFullQueryTerms);
88                 }
89             } else {
90                 // Strip excess whitespace.
91                 query = query.trim();
92             }
93             ArrayList<Cursor> cursors = new ArrayList<Cursor>();
94             // Pass query; at this point it is either the last term OR the
95             // only term.
96             final Cursor c = super.query(query);
97             if (c != null) {
98                 cursors.add(c);
99             }
100 
101             if (query.length() >= MIN_QUERY_LENGTH_FOR_CONTACTS) {
102                 cursors.add(new ContactsCursor().query(query));
103             }
104 
105             if (cursors.size() > 0) {
106                 mergeCursor = new MergeCursor(cursors.toArray(new Cursor[cursors.size()]));
107             }
108         }
109         return mergeCursor;
110     }
111 
112     /**
113      * Utility class to return a cursor over the contacts database
114      */
115     private final class ContactsCursor extends MatrixCursorWithCachedColumns {
ContactsCursor()116         public ContactsCursor() {
117             super(CONTACTS_COLUMNS);
118         }
119 
120         /**
121          * Searches over the contacts cursor with the specified query as the starting characters to
122          * match.
123          * @param query
124          * @return a cursor over the contacts database with the contacts matching the query.
125          */
query(String query)126         public ContactsCursor query(String query) {
127             final Uri contactsUri = Uri.withAppendedPath(
128                     ContactsContract.CommonDataKinds.Email.CONTENT_FILTER_URI, Uri.encode(query));
129             final Cursor cursor = mContext.getContentResolver().query(
130                     contactsUri, sContract, null, null, null);
131             // We don't want to show a contact icon here. Leaving the SEARCH_ICON_1 field
132             // empty causes inconsistent behavior because the cursor is merged with the
133             // historical suggestions, which have an icon.  The solution is to show an empty icon
134             // instead.
135             final String emptyIcon = ContentResolver.SCHEME_ANDROID_RESOURCE + "://"
136                     + mContext.getPackageName() + "/" + R.drawable.empty;
137             if (cursor != null) {
138                 final int nameIndex = cursor
139                         .getColumnIndex(ContactsContract.CommonDataKinds.Email.DISPLAY_NAME);
140                 final int addressIndex = cursor
141                         .getColumnIndex(ContactsContract.CommonDataKinds.Email.DATA);
142                 String match;
143                 while (cursor.moveToNext()) {
144                     match = cursor.getString(nameIndex);
145                     match = !TextUtils.isEmpty(match) ? match : cursor.getString(addressIndex);
146                     // The order of fields is:
147                     // _ID, SUGGEST_COLUMN_TEXT_1, SUGGEST_COLUMN_QUERY, SUGGEST_COLUMN_ICON_1
148                     addRow(new Object[] {0, match, createQuery(match), emptyIcon});
149                 }
150                 cursor.close();
151             }
152             return this;
153         }
154     }
155 
createQuery(String inMatch)156     private String createQuery(String inMatch) {
157         final StringBuilder query = new StringBuilder();
158         if (mFullQueryTerms != null) {
159             synchronized (mTermsLock) {
160                 for (String token : mFullQueryTerms) {
161                     query.append(token).append(QUERY_TOKEN_SEPARATOR);
162                 }
163             }
164         }
165         // Append the match as well.
166         query.append(inMatch);
167         // Example:
168         // Search terms in the searchbox are : "pdf test*"
169         // Contacts database contains: test@tester.com, test@other.com
170         // If the user taps "test@tester.com", the query passed with
171         // ACTION_SEARCH is:
172         // "pdf test@tester.com"
173         // If the user taps "test@other.com", the query passed with
174         // ACTION_SEARCH is:
175         // "pdf test@other.com"
176         return query.toString();
177     }
178 }