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 SuggestionsProvider()68 public SuggestionsProvider() { 69 super(); 70 } 71 72 @Override onCreate()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 query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)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; ContactsCursor()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 */ query(String query)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 createQuery(String inMatch)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@tester.com, test@other.com 183 // If the user taps "test@tester.com", the query passed with 184 // ACTION_SEARCH is: 185 // "pdf test@tester.com" 186 // If the user taps "test@other.com", the query passed with 187 // ACTION_SEARCH is: 188 // "pdf test@other.com" 189 return query.toString(); 190 } 191 }