GlobalSearchSupport.java revision b38ed2c5ffeb20efc677b4a9229db4a00603aa8d
1/* 2 * Copyright (C) 2009 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 17package com.android.providers.contacts; 18 19import com.android.internal.database.ArrayListCursor; 20import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns; 21import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns; 22import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns; 23import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns; 24import com.android.providers.contacts.ContactsDatabaseHelper.Tables; 25 26import android.app.SearchManager; 27import android.content.ContentUris; 28import android.content.res.Resources; 29import android.database.Cursor; 30import android.database.sqlite.SQLiteDatabase; 31import android.net.Uri; 32import android.provider.Contacts.Intents; 33import android.provider.ContactsContract.Contacts; 34import android.provider.ContactsContract.Data; 35import android.provider.ContactsContract.Presence; 36import android.provider.ContactsContract.RawContacts; 37import android.provider.ContactsContract.StatusUpdates; 38import android.provider.ContactsContract.CommonDataKinds.Email; 39import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 40import android.provider.ContactsContract.CommonDataKinds.Organization; 41import android.provider.ContactsContract.CommonDataKinds.Phone; 42import android.provider.ContactsContract.CommonDataKinds.StructuredName; 43import android.provider.ContactsContract.Contacts.Photo; 44import android.text.TextUtils; 45 46import java.util.ArrayList; 47import java.util.Collections; 48import java.util.Comparator; 49import java.util.HashMap; 50 51/** 52 * Support for global search integration for Contacts. 53 */ 54public class GlobalSearchSupport { 55 56 private static final String[] SEARCH_SUGGESTIONS_BASED_ON_PHONE_NUMBER_COLUMNS = { 57 "_id", 58 SearchManager.SUGGEST_COLUMN_TEXT_1, 59 SearchManager.SUGGEST_COLUMN_TEXT_2, 60 SearchManager.SUGGEST_COLUMN_ICON_1, 61 SearchManager.SUGGEST_COLUMN_INTENT_DATA, 62 SearchManager.SUGGEST_COLUMN_INTENT_ACTION, 63 SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, 64 }; 65 66 private static final String[] SEARCH_SUGGESTIONS_BASED_ON_NAME_COLUMNS = { 67 "_id", 68 SearchManager.SUGGEST_COLUMN_TEXT_1, 69 SearchManager.SUGGEST_COLUMN_TEXT_2, 70 SearchManager.SUGGEST_COLUMN_ICON_1, 71 SearchManager.SUGGEST_COLUMN_ICON_2, 72 SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID, 73 SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, 74 }; 75 76 private interface SearchSuggestionQuery { 77 public static final String JOIN_RAW_CONTACTS = 78 " JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "; 79 80 public static final String JOIN_CONTACTS = 81 " JOIN contacts ON (raw_contacts.contact_id = contacts._id)"; 82 83 public static final String JOIN_MIMETYPES = 84 " JOIN mimetypes ON (data.mimetype_id = mimetypes._id AND mimetypes.mimetype IN ('" 85 + StructuredName.CONTENT_ITEM_TYPE + "','" + Email.CONTENT_ITEM_TYPE + "','" 86 + Phone.CONTENT_ITEM_TYPE + "','" + Organization.CONTENT_ITEM_TYPE + "','" 87 + GroupMembership.CONTENT_ITEM_TYPE + "')) "; 88 89 public static final String TABLE = "data " + JOIN_RAW_CONTACTS + JOIN_MIMETYPES 90 + JOIN_CONTACTS; 91 92 public static final String PRESENCE_SQL = 93 "(SELECT " + StatusUpdates.PRESENCE_STATUS + 94 " FROM " + Tables.AGGREGATED_PRESENCE + 95 " WHERE " + AggregatedPresenceColumns.CONTACT_ID 96 + "=" + ContactsColumns.CONCRETE_ID + ")"; 97 98 public static final String[] COLUMNS = { 99 ContactsColumns.CONCRETE_ID + " AS " + Contacts._ID, 100 ContactsColumns.CONCRETE_DISPLAY_NAME + " AS " + Contacts.DISPLAY_NAME, 101 PRESENCE_SQL + " AS " + Contacts.CONTACT_PRESENCE, 102 DataColumns.CONCRETE_ID + " AS data_id", 103 MimetypesColumns.MIMETYPE, 104 Data.IS_SUPER_PRIMARY, 105 Organization.COMPANY, 106 Email.DATA, 107 Phone.NUMBER, 108 Contacts.PHOTO_ID, 109 }; 110 111 public static final int CONTACT_ID = 0; 112 public static final int DISPLAY_NAME = 1; 113 public static final int PRESENCE_STATUS = 2; 114 public static final int DATA_ID = 3; 115 public static final int MIMETYPE = 4; 116 public static final int IS_SUPER_PRIMARY = 5; 117 public static final int ORGANIZATION = 6; 118 public static final int EMAIL = 7; 119 public static final int PHONE = 8; 120 public static final int PHOTO_ID = 9; 121 } 122 123 private static class SearchSuggestion { 124 String contactId; 125 boolean titleIsName; 126 String organization; 127 String email; 128 String phoneNumber; 129 Uri photoUri; 130 String normalizedName; 131 int presence = -1; 132 boolean processed; 133 String text1; 134 String text2; 135 String icon1; 136 String icon2; 137 138 public SearchSuggestion(long contactId) { 139 this.contactId = String.valueOf(contactId); 140 } 141 142 private void process() { 143 if (processed) { 144 return; 145 } 146 147 boolean hasOrganization = !TextUtils.isEmpty(organization); 148 boolean hasEmail = !TextUtils.isEmpty(email); 149 boolean hasPhone = !TextUtils.isEmpty(phoneNumber); 150 151 boolean titleIsOrganization = !titleIsName && hasOrganization; 152 boolean titleIsEmail = !titleIsName && !titleIsOrganization && hasEmail; 153 boolean titleIsPhone = !titleIsName && !titleIsOrganization && !titleIsEmail 154 && hasPhone; 155 156 if (!titleIsOrganization && hasOrganization) { 157 text2 = organization; 158 } else if (!titleIsEmail && hasEmail) { 159 text2 = email; 160 } else if (!titleIsPhone && hasPhone) { 161 text2 = phoneNumber; 162 } 163 164 if (photoUri != null) { 165 icon1 = photoUri.toString(); 166 } else { 167 icon1 = String.valueOf(com.android.internal.R.drawable.ic_contact_picture); 168 } 169 170 if (presence != -1) { 171 icon2 = String.valueOf(StatusUpdates.getPresenceIconResourceId(presence)); 172 } 173 174 processed = true; 175 } 176 177 public String getSortKey() { 178 if (normalizedName == null) { 179 process(); 180 normalizedName = text1 == null ? "" : NameNormalizer.normalize(text1); 181 } 182 return normalizedName; 183 } 184 185 @SuppressWarnings({"unchecked"}) 186 public ArrayList asList(String[] projection) { 187 process(); 188 189 ArrayList<Object> list = new ArrayList<Object>(); 190 if (projection == null) { 191 list.add(contactId); 192 list.add(text1); 193 list.add(text2); 194 list.add(icon1); 195 list.add(icon2); 196 list.add(contactId); 197 list.add(contactId); 198 } else { 199 for (int i = 0; i < projection.length; i++) { 200 addColumnValue(list, projection[i]); 201 } 202 } 203 return list; 204 } 205 206 private void addColumnValue(ArrayList<Object> list, String column) { 207 if ("_id".equals(column)) { 208 list.add(contactId); 209 } else if (SearchManager.SUGGEST_COLUMN_TEXT_1.equals(column)) { 210 list.add(text1); 211 } else if (SearchManager.SUGGEST_COLUMN_TEXT_2.equals(column)) { 212 list.add(text2); 213 } else if (SearchManager.SUGGEST_COLUMN_ICON_1.equals(column)) { 214 list.add(icon1); 215 } else if (SearchManager.SUGGEST_COLUMN_ICON_2.equals(column)) { 216 list.add(icon2); 217 } else if (SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID.equals(column)) { 218 list.add(contactId); 219 } else if (SearchManager.SUGGEST_COLUMN_SHORTCUT_ID.equals(column)) { 220 list.add(contactId); 221 } else { 222 throw new IllegalArgumentException("Invalid column name: " + column); 223 } 224 } 225 } 226 227 private final ContactsProvider2 mContactsProvider; 228 229 public GlobalSearchSupport(ContactsProvider2 contactsProvider) { 230 mContactsProvider = contactsProvider; 231 } 232 233 public Cursor handleSearchSuggestionsQuery(SQLiteDatabase db, Uri uri, String limit) { 234 if (uri.getPathSegments().size() <= 1) { 235 return null; 236 } 237 238 final String searchClause = uri.getLastPathSegment(); 239 if (TextUtils.isDigitsOnly(searchClause)) { 240 return buildCursorForSearchSuggestionsBasedOnPhoneNumber(searchClause); 241 } else { 242 return buildCursorForSearchSuggestionsBasedOnName(db, searchClause, limit); 243 } 244 } 245 246 public Cursor handleSearchShortcutRefresh(SQLiteDatabase db, long contactId, String[] projection) { 247 StringBuilder sb = new StringBuilder(); 248 sb.append(mContactsProvider.getContactsRestrictions()); 249 sb.append(" AND " + RawContacts.CONTACT_ID + "=" + contactId); 250 return buildCursorForSearchSuggestions(db, sb.toString(), projection); 251 } 252 253 private Cursor buildCursorForSearchSuggestionsBasedOnPhoneNumber(String searchClause) { 254 Resources r = mContactsProvider.getContext().getResources(); 255 String s; 256 int i; 257 258 ArrayList<Object> dialNumber = new ArrayList<Object>(); 259 dialNumber.add(0); // _id 260 s = r.getString(com.android.internal.R.string.dial_number_using, searchClause); 261 i = s.indexOf('\n'); 262 if (i < 0) { 263 dialNumber.add(s); 264 dialNumber.add(""); 265 } else { 266 dialNumber.add(s.substring(0, i)); 267 dialNumber.add(s.substring(i + 1)); 268 } 269 dialNumber.add(String.valueOf(com.android.internal.R.drawable.call_contact)); 270 dialNumber.add("tel:" + searchClause); 271 dialNumber.add(Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED); 272 dialNumber.add(null); 273 274 ArrayList<Object> createContact = new ArrayList<Object>(); 275 createContact.add(1); // _id 276 s = r.getString(com.android.internal.R.string.create_contact_using, searchClause); 277 i = s.indexOf('\n'); 278 if (i < 0) { 279 createContact.add(s); 280 createContact.add(""); 281 } else { 282 createContact.add(s.substring(0, i)); 283 createContact.add(s.substring(i + 1)); 284 } 285 createContact.add(String.valueOf(com.android.internal.R.drawable.create_contact)); 286 createContact.add("tel:" + searchClause); 287 createContact.add(Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED); 288 createContact.add(SearchManager.SUGGEST_NEVER_MAKE_SHORTCUT); 289 290 @SuppressWarnings({"unchecked"}) ArrayList<ArrayList> rows = new ArrayList<ArrayList>(); 291 rows.add(dialNumber); 292 rows.add(createContact); 293 294 return new ArrayListCursor(SEARCH_SUGGESTIONS_BASED_ON_PHONE_NUMBER_COLUMNS, rows); 295 } 296 297 private Cursor buildCursorForSearchSuggestionsBasedOnName(SQLiteDatabase db, 298 String searchClause, String limit) { 299 300 StringBuilder sb = new StringBuilder(); 301 sb.append(mContactsProvider.getContactsRestrictions()); 302 sb.append(" AND " + DataColumns.CONCRETE_RAW_CONTACT_ID + " IN "); 303 mContactsProvider.appendRawContactsByFilterAsNestedQuery(sb, searchClause, limit); 304 sb.append(" AND " + Contacts.IN_VISIBLE_GROUP + "=1"); 305 306 return buildCursorForSearchSuggestions(db, sb.toString(), null); 307 } 308 309 private Cursor buildCursorForSearchSuggestions(SQLiteDatabase db, String selection, 310 String[] projection) { 311 ArrayList<SearchSuggestion> suggestionList = new ArrayList<SearchSuggestion>(); 312 HashMap<Long, SearchSuggestion> suggestionMap = new HashMap<Long, SearchSuggestion>(); 313 Cursor c = db.query(true, SearchSuggestionQuery.TABLE, 314 SearchSuggestionQuery.COLUMNS, selection, null, null, null, null, null); 315 try { 316 while (c.moveToNext()) { 317 318 long contactId = c.getLong(SearchSuggestionQuery.CONTACT_ID); 319 SearchSuggestion suggestion = suggestionMap.get(contactId); 320 if (suggestion == null) { 321 suggestion = new SearchSuggestion(contactId); 322 suggestionList.add(suggestion); 323 suggestionMap.put(contactId, suggestion); 324 } 325 326 boolean isSuperPrimary = c.getInt(SearchSuggestionQuery.IS_SUPER_PRIMARY) != 0; 327 suggestion.text1 = c.getString(SearchSuggestionQuery.DISPLAY_NAME); 328 329 if (!c.isNull(SearchSuggestionQuery.PRESENCE_STATUS)) { 330 suggestion.presence = c.getInt(SearchSuggestionQuery.PRESENCE_STATUS); 331 } 332 333 String mimetype = c.getString(SearchSuggestionQuery.MIMETYPE); 334 if (StructuredName.CONTENT_ITEM_TYPE.equals(mimetype)) { 335 suggestion.titleIsName = true; 336 } else if (Organization.CONTENT_ITEM_TYPE.equals(mimetype)) { 337 if (isSuperPrimary || suggestion.organization == null) { 338 suggestion.organization = c.getString(SearchSuggestionQuery.ORGANIZATION); 339 } 340 } else if (Email.CONTENT_ITEM_TYPE.equals(mimetype)) { 341 if (isSuperPrimary || suggestion.email == null) { 342 suggestion.email = c.getString(SearchSuggestionQuery.EMAIL); 343 } 344 } else if (Phone.CONTENT_ITEM_TYPE.equals(mimetype)) { 345 if (isSuperPrimary || suggestion.phoneNumber == null) { 346 suggestion.phoneNumber = c.getString(SearchSuggestionQuery.PHONE); 347 } 348 } 349 350 if (!c.isNull(SearchSuggestionQuery.PHOTO_ID)) { 351 suggestion.photoUri = Uri.withAppendedPath( 352 ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId), 353 Photo.CONTENT_DIRECTORY); 354 } 355 } 356 } finally { 357 c.close(); 358 } 359 360 Collections.sort(suggestionList, new Comparator<SearchSuggestion>() { 361 public int compare(SearchSuggestion row1, SearchSuggestion row2) { 362 return row1.getSortKey().compareTo(row2.getSortKey()); 363 } 364 }); 365 366 @SuppressWarnings({"unchecked"}) ArrayList<ArrayList> rows = new ArrayList<ArrayList>(); 367 for (int i = 0; i < suggestionList.size(); i++) { 368 rows.add(suggestionList.get(i).asList(projection)); 369 } 370 371 return new ArrayListCursor(projection != null ? projection 372 : SEARCH_SUGGESTIONS_BASED_ON_NAME_COLUMNS, rows); 373 } 374} 375