GlobalSearchSupport.java revision 5bd028407806015c91d863ee2bbffbaaf1c200d8
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.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns; 20import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns; 21import com.android.providers.contacts.ContactsDatabaseHelper.Tables; 22 23import android.app.SearchManager; 24import android.content.res.Resources; 25import android.database.Cursor; 26import android.database.MatrixCursor; 27import android.database.sqlite.SQLiteDatabase; 28import android.net.Uri; 29import android.provider.ContactsContract; 30import android.provider.ContactsContract.CommonDataKinds.Email; 31import android.provider.ContactsContract.CommonDataKinds.Organization; 32import android.provider.ContactsContract.CommonDataKinds.Phone; 33import android.provider.ContactsContract.Contacts; 34import android.provider.ContactsContract.Data; 35import android.provider.ContactsContract.SearchSnippetColumns; 36import android.provider.ContactsContract.StatusUpdates; 37import android.text.TextUtils; 38 39import java.util.ArrayList; 40 41/** 42 * Support for global search integration for Contacts. 43 */ 44public class GlobalSearchSupport { 45 46 private static final String[] SEARCH_SUGGESTIONS_BASED_ON_PHONE_NUMBER_COLUMNS = { 47 "_id", 48 SearchManager.SUGGEST_COLUMN_TEXT_1, 49 SearchManager.SUGGEST_COLUMN_TEXT_2, 50 SearchManager.SUGGEST_COLUMN_ICON_1, 51 SearchManager.SUGGEST_COLUMN_INTENT_DATA, 52 SearchManager.SUGGEST_COLUMN_INTENT_ACTION, 53 SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, 54 }; 55 56 private static final String[] SEARCH_SUGGESTIONS_BASED_ON_NAME_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_ICON_2, 62 SearchManager.SUGGEST_COLUMN_INTENT_DATA, 63 SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, 64 SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA, 65 }; 66 67 private static final char SNIPPET_START_MATCH = '\u0001'; 68 private static final char SNIPPET_END_MATCH = '\u0001'; 69 private static final String SNIPPET_ELLIPSIS = "\u2026"; 70 private static final int SNIPPET_MAX_TOKENS = 5; 71 72 private static final String PRESENCE_SQL = 73 "(SELECT " + StatusUpdates.PRESENCE + 74 " FROM " + Tables.AGGREGATED_PRESENCE + 75 " WHERE " + AggregatedPresenceColumns.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + ")"; 76 77 // Current contacts - those contacted within the last 3 days (in seconds) 78 private static final long CURRENT_CONTACTS = 3 * 24 * 60 * 60; 79 80 // Recent contacts - those contacted within the last 30 days (in seconds) 81 private static final long RECENT_CONTACTS = 30 * 24 * 60 * 60; 82 83 private static final String TIME_SINCE_LAST_CONTACTED = 84 "(strftime('%s', 'now') - contacts." + Contacts.LAST_TIME_CONTACTED + "/1000)"; 85 86 /* 87 * See {@link ContactsProvider2#EMAIL_FILTER_SORT_ORDER} for the discussion of this 88 * sorting order. 89 */ 90 private static final String SORT_ORDER = 91 "(CASE WHEN contacts." + Contacts.STARRED + "=1 THEN 0 ELSE 1 END), " 92 + "(CASE WHEN " + TIME_SINCE_LAST_CONTACTED + " < " + CURRENT_CONTACTS + " THEN 0 " 93 + " WHEN " + TIME_SINCE_LAST_CONTACTED + " < " + RECENT_CONTACTS + " THEN 1 " 94 + " ELSE 2 END)," 95 + "contacts." + Contacts.TIMES_CONTACTED + " DESC, " 96 + "contacts." + Contacts.DISPLAY_NAME_PRIMARY + ", " 97 + "contacts." + Contacts._ID; 98 99 private static final String RECENTLY_CONTACTED = 100 TIME_SINCE_LAST_CONTACTED + " < " + RECENT_CONTACTS; 101 102 private static class SearchSuggestion { 103 long contactId; 104 String photoUri; 105 String lookupKey; 106 int presence = -1; 107 String text1; 108 String text2; 109 String icon1; 110 String icon2; 111 String filter; 112 113 @SuppressWarnings({"unchecked"}) 114 public ArrayList asList(String[] projection) { 115 if (photoUri != null) { 116 icon1 = photoUri.toString(); 117 } else { 118 icon1 = String.valueOf(com.android.internal.R.drawable.ic_contact_picture); 119 } 120 121 if (presence != -1) { 122 icon2 = String.valueOf(StatusUpdates.getPresenceIconResourceId(presence)); 123 } 124 125 ArrayList<Object> list = new ArrayList<Object>(); 126 if (projection == null) { 127 list.add(contactId); 128 list.add(text1); 129 list.add(text2); 130 list.add(icon1); 131 list.add(icon2); 132 list.add(buildUri()); 133 list.add(lookupKey); 134 list.add(filter); 135 } else { 136 for (int i = 0; i < projection.length; i++) { 137 addColumnValue(list, projection[i]); 138 } 139 } 140 return list; 141 } 142 143 private void addColumnValue(ArrayList<Object> list, String column) { 144 if ("_id".equals(column)) { 145 list.add(contactId); 146 } else if (SearchManager.SUGGEST_COLUMN_TEXT_1.equals(column)) { 147 list.add(text1); 148 } else if (SearchManager.SUGGEST_COLUMN_TEXT_2.equals(column)) { 149 list.add(text2); 150 } else if (SearchManager.SUGGEST_COLUMN_ICON_1.equals(column)) { 151 list.add(icon1); 152 } else if (SearchManager.SUGGEST_COLUMN_ICON_2.equals(column)) { 153 list.add(icon2); 154 } else if (SearchManager.SUGGEST_COLUMN_INTENT_DATA.equals(column)) { 155 list.add(buildUri()); 156 } else if (SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID.equals(column)) { 157 list.add(lookupKey); 158 } else if (SearchManager.SUGGEST_COLUMN_SHORTCUT_ID.equals(column)) { 159 list.add(lookupKey); 160 } else if (SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA.equals(column)) { 161 list.add(filter); 162 } else { 163 throw new IllegalArgumentException("Invalid column name: " + column); 164 } 165 } 166 167 private String buildUri() { 168 return Contacts.getLookupUri(contactId, lookupKey).toString(); 169 } 170 } 171 172 private final ContactsProvider2 mContactsProvider; 173 174 @SuppressWarnings("all") 175 public GlobalSearchSupport(ContactsProvider2 contactsProvider) { 176 mContactsProvider = contactsProvider; 177 178 // To ensure the data column position. This is dead code if properly configured. 179 if (Organization.COMPANY != Data.DATA1 || Phone.NUMBER != Data.DATA1 180 || Email.DATA != Data.DATA1) { 181 throw new AssertionError("Some of ContactsContract.CommonDataKinds class primary" 182 + " data is not in DATA1 column"); 183 } 184 } 185 186 public Cursor handleSearchSuggestionsQuery( 187 SQLiteDatabase db, Uri uri, String[] projection, String limit) { 188 final String searchClause; 189 final String selection; 190 if (uri.getPathSegments().size() <= 1) { 191 searchClause = null; 192 selection = RECENTLY_CONTACTED; 193 } else { 194 searchClause = uri.getLastPathSegment(); 195 selection = null; 196 } 197 198 if (!TextUtils.isEmpty(searchClause) && TextUtils.isDigitsOnly(searchClause) 199 && mContactsProvider.isPhone()) { 200 return buildCursorForSearchSuggestionsBasedOnPhoneNumber(searchClause); 201 } else { 202 return buildCursorForSearchSuggestionsBasedOnFilter( 203 db, projection, selection, searchClause, limit); 204 } 205 } 206 207 /** 208 * Returns a search suggestions cursor for the contact bearing the provided lookup key. If the 209 * lookup key cannot be found in the database, the contact name is decoded from the lookup key 210 * and used to re-identify the contact. If the contact still cannot be found, an empty cursor 211 * is returned. 212 * 213 * <p>Note that if {@code lookupKey} is not a valid lookup key, an empty cursor is returned 214 * silently. This would occur with old-style shortcuts that were created using the contact id 215 * instead of the lookup key. 216 */ 217 public Cursor handleSearchShortcutRefresh(SQLiteDatabase db, String[] projection, 218 String lookupKey, String filter) { 219 long contactId; 220 try { 221 contactId = mContactsProvider.lookupContactIdByLookupKey(db, lookupKey); 222 } catch (IllegalArgumentException e) { 223 contactId = -1L; 224 } 225 return buildCursorForSearchSuggestionsBasedOnFilter( 226 db, projection, ContactsColumns.CONCRETE_ID + "=" + contactId, filter, null); 227 } 228 229 private Cursor buildCursorForSearchSuggestionsBasedOnPhoneNumber(String searchClause) { 230 MatrixCursor cursor = new MatrixCursor(SEARCH_SUGGESTIONS_BASED_ON_PHONE_NUMBER_COLUMNS); 231 Resources r = mContactsProvider.getContext().getResources(); 232 String s; 233 int i; 234 235 ArrayList<Object> dialNumber = new ArrayList<Object>(); 236 dialNumber.add(0); // _id 237 s = r.getString(com.android.internal.R.string.dial_number_using, searchClause); 238 i = s.indexOf('\n'); 239 if (i < 0) { 240 dialNumber.add(s); 241 dialNumber.add(""); 242 } else { 243 dialNumber.add(s.substring(0, i)); 244 dialNumber.add(s.substring(i + 1)); 245 } 246 dialNumber.add(String.valueOf(com.android.internal.R.drawable.call_contact)); 247 dialNumber.add("tel:" + searchClause); 248 dialNumber.add(ContactsContract.Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED); 249 dialNumber.add(null); 250 cursor.addRow(dialNumber); 251 252 ArrayList<Object> createContact = new ArrayList<Object>(); 253 createContact.add(1); // _id 254 s = r.getString(com.android.internal.R.string.create_contact_using, searchClause); 255 i = s.indexOf('\n'); 256 if (i < 0) { 257 createContact.add(s); 258 createContact.add(""); 259 } else { 260 createContact.add(s.substring(0, i)); 261 createContact.add(s.substring(i + 1)); 262 } 263 createContact.add(String.valueOf(com.android.internal.R.drawable.create_contact)); 264 createContact.add("tel:" + searchClause); 265 createContact.add(ContactsContract.Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED); 266 createContact.add(SearchManager.SUGGEST_NEVER_MAKE_SHORTCUT); 267 cursor.addRow(createContact); 268 269 return cursor; 270 } 271 272 private Cursor buildCursorForSearchSuggestionsBasedOnFilter(SQLiteDatabase db, 273 String[] projection, String selection, String filter, String limit) { 274 MatrixCursor cursor = new MatrixCursor( 275 projection != null ? projection : SEARCH_SUGGESTIONS_BASED_ON_NAME_COLUMNS); 276 StringBuilder sb = new StringBuilder(); 277 final boolean haveFilter = !TextUtils.isEmpty(filter); 278 sb.append("SELECT " 279 + Contacts._ID + ", " 280 + Contacts.LOOKUP_KEY + ", " 281 + Contacts.PHOTO_THUMBNAIL_URI + ", " 282 + Contacts.DISPLAY_NAME + ", " 283 + PRESENCE_SQL + " AS " + Contacts.CONTACT_PRESENCE); 284 if (haveFilter) { 285 sb.append(", " + SearchSnippetColumns.SNIPPET); 286 } 287 sb.append(" FROM "); 288 sb.append(getDatabaseHelper().getContactView(false)); 289 sb.append(" AS contacts"); 290 if (haveFilter) { 291 mContactsProvider.appendSearchIndexJoin(sb, filter, true, 292 String.valueOf(SNIPPET_START_MATCH), String.valueOf(SNIPPET_END_MATCH), 293 SNIPPET_ELLIPSIS, SNIPPET_MAX_TOKENS); 294 } 295 if (selection != null) { 296 sb.append(" WHERE ").append(selection); 297 } 298 sb.append(" ORDER BY " + SORT_ORDER); 299 if (limit != null) { 300 sb.append(" LIMIT " + limit); 301 } 302 Cursor c = new SnippetizingCursorWrapper( 303 db.rawQuery(sb.toString(), null), 304 haveFilter ? filter : "", 305 String.valueOf(SNIPPET_START_MATCH), 306 String.valueOf(SNIPPET_END_MATCH), 307 SNIPPET_ELLIPSIS, 308 SNIPPET_MAX_TOKENS); 309 SearchSuggestion suggestion = new SearchSuggestion(); 310 suggestion.filter = filter; 311 try { 312 while (c.moveToNext()) { 313 suggestion.contactId = c.getLong(0); 314 suggestion.lookupKey = c.getString(1); 315 suggestion.photoUri = c.getString(2); 316 suggestion.text1 = c.getString(3); 317 suggestion.presence = c.isNull(4) ? -1 : c.getInt(4); 318 if (haveFilter) { 319 suggestion.text2 = shortenSnippet(c.getString(5)); 320 } 321 cursor.addRow(suggestion.asList(projection)); 322 } 323 } finally { 324 c.close(); 325 } 326 return cursor; 327 } 328 329 private ContactsDatabaseHelper getDatabaseHelper() { 330 return (ContactsDatabaseHelper) mContactsProvider.getDatabaseHelper(); 331 } 332 333 private String shortenSnippet(final String snippet) { 334 if (snippet == null) { 335 return null; 336 } 337 338 int from = 0; 339 int to = snippet.length(); 340 int start = snippet.indexOf(SNIPPET_START_MATCH); 341 if (start == -1) { 342 return null; 343 } 344 345 int firstNl = snippet.lastIndexOf('\n', start); 346 if (firstNl != -1) { 347 from = firstNl + 1; 348 } 349 int end = snippet.lastIndexOf(SNIPPET_END_MATCH); 350 if (end != -1) { 351 int lastNl = snippet.indexOf('\n', end); 352 if (lastNl != -1) { 353 to = lastNl; 354 } 355 } 356 357 StringBuilder sb = new StringBuilder(); 358 for (int i = from; i < to; i++) { 359 char c = snippet.charAt(i); 360 if (c != SNIPPET_START_MATCH && c != SNIPPET_END_MATCH) { 361 sb.append(c); 362 } 363 } 364 return sb.toString(); 365 } 366} 367