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