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 }