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 17 package com.android.providers.contacts; 18 19 import android.app.SearchManager; 20 import android.content.Context; 21 import android.database.Cursor; 22 import android.database.MatrixCursor; 23 import android.database.sqlite.SQLiteDatabase; 24 import android.net.Uri; 25 import android.os.CancellationSignal; 26 import android.provider.ContactsContract.CommonDataKinds.Email; 27 import android.provider.ContactsContract.CommonDataKinds.Organization; 28 import android.provider.ContactsContract.CommonDataKinds.Phone; 29 import android.provider.ContactsContract.Contacts; 30 import android.provider.ContactsContract.Data; 31 import android.provider.ContactsContract.SearchSnippets; 32 import android.provider.ContactsContract.StatusUpdates; 33 import android.telephony.TelephonyManager; 34 import android.text.TextUtils; 35 36 import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns; 37 import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns; 38 import com.android.providers.contacts.ContactsDatabaseHelper.Tables; 39 import com.android.providers.contacts.ContactsDatabaseHelper.Views; 40 41 import java.util.ArrayList; 42 43 /** 44 * Support for global search integration for Contacts. 45 */ 46 public class GlobalSearchSupport { 47 48 private static final String[] SEARCH_SUGGESTIONS_COLUMNS = { 49 "_id", 50 SearchManager.SUGGEST_COLUMN_TEXT_1, 51 SearchManager.SUGGEST_COLUMN_TEXT_2, 52 SearchManager.SUGGEST_COLUMN_ICON_1, 53 SearchManager.SUGGEST_COLUMN_ICON_2, 54 SearchManager.SUGGEST_COLUMN_INTENT_DATA, 55 SearchManager.SUGGEST_COLUMN_INTENT_ACTION, 56 SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, 57 SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA, 58 SearchManager.SUGGEST_COLUMN_LAST_ACCESS_HINT, 59 }; 60 61 private static final char SNIPPET_START_MATCH = '\u0001'; 62 private static final char SNIPPET_END_MATCH = '\u0001'; 63 private static final String SNIPPET_ELLIPSIS = "\u2026"; 64 private static final int SNIPPET_MAX_TOKENS = 5; 65 66 private static final String PRESENCE_SQL = 67 "(SELECT " + StatusUpdates.PRESENCE + 68 " FROM " + Tables.AGGREGATED_PRESENCE + 69 " WHERE " + AggregatedPresenceColumns.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + ")"; 70 71 private static class SearchSuggestion { 72 long contactId; 73 String photoUri; 74 String lookupKey; 75 int presence = -1; 76 String text1; 77 String text2; 78 String icon1; 79 String icon2; 80 String intentData; 81 String intentAction; 82 String filter; 83 String lastAccessTime; 84 85 @SuppressWarnings({"unchecked"}) asList(String[] projection)86 public ArrayList<?> asList(String[] projection) { 87 if (icon1 == null) { 88 if (photoUri != null) { 89 icon1 = photoUri.toString(); 90 } else { 91 icon1 = String.valueOf(com.android.internal.R.drawable.ic_contact_picture); 92 } 93 } 94 95 if (presence != -1) { 96 icon2 = String.valueOf(StatusUpdates.getPresenceIconResourceId(presence)); 97 } 98 99 ArrayList<Object> list = new ArrayList<Object>(); 100 if (projection == null) { 101 list.add(contactId); // _id 102 list.add(text1); // text1 103 list.add(text2); // text2 104 list.add(icon1); // icon1 105 list.add(icon2); // icon2 106 list.add(intentData == null ? buildUri() : intentData); // intent data 107 list.add(intentAction); // intentAction 108 list.add(lookupKey); // shortcut id 109 list.add(filter); // extra data 110 list.add(lastAccessTime); // last access hint 111 } else { 112 for (int i = 0; i < projection.length; i++) { 113 addColumnValue(list, projection[i]); 114 } 115 } 116 return list; 117 } 118 addColumnValue(ArrayList<Object> list, String column)119 private void addColumnValue(ArrayList<Object> list, String column) { 120 if ("_id".equals(column)) { 121 list.add(contactId); 122 } else if (SearchManager.SUGGEST_COLUMN_TEXT_1.equals(column)) { 123 list.add(text1); 124 } else if (SearchManager.SUGGEST_COLUMN_TEXT_2.equals(column)) { 125 list.add(text2); 126 } else if (SearchManager.SUGGEST_COLUMN_ICON_1.equals(column)) { 127 list.add(icon1); 128 } else if (SearchManager.SUGGEST_COLUMN_ICON_2.equals(column)) { 129 list.add(icon2); 130 } else if (SearchManager.SUGGEST_COLUMN_INTENT_DATA.equals(column)) { 131 list.add(intentData == null ? buildUri() : intentData); 132 } else if (SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID.equals(column)) { 133 list.add(lookupKey); 134 } else if (SearchManager.SUGGEST_COLUMN_SHORTCUT_ID.equals(column)) { 135 list.add(lookupKey); 136 } else if (SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA.equals(column)) { 137 list.add(filter); 138 } else if (SearchManager.SUGGEST_COLUMN_LAST_ACCESS_HINT.equals(column)) { 139 list.add(lastAccessTime); 140 } else { 141 throw new IllegalArgumentException("Invalid column name: " + column); 142 } 143 } 144 buildUri()145 private String buildUri() { 146 return Contacts.getLookupUri(contactId, lookupKey).toString(); 147 } 148 reset()149 public void reset() { 150 contactId = 0; 151 photoUri = null; 152 lookupKey = null; 153 presence = -1; 154 text1 = null; 155 text2 = null; 156 icon1 = null; 157 icon2 = null; 158 intentData = null; 159 intentAction = null; 160 filter = null; 161 lastAccessTime = null; 162 } 163 } 164 165 private final ContactsProvider2 mContactsProvider; 166 167 @SuppressWarnings("all") GlobalSearchSupport(ContactsProvider2 contactsProvider)168 public GlobalSearchSupport(ContactsProvider2 contactsProvider) { 169 mContactsProvider = contactsProvider; 170 171 TelephonyManager telman = (TelephonyManager) 172 mContactsProvider.getContext().getSystemService(Context.TELEPHONY_SERVICE); 173 174 // To ensure the data column position. This is dead code if properly configured. 175 if (Organization.COMPANY != Data.DATA1 || Phone.NUMBER != Data.DATA1 176 || Email.DATA != Data.DATA1) { 177 throw new AssertionError("Some of ContactsContract.CommonDataKinds class primary" 178 + " data is not in DATA1 column"); 179 } 180 } 181 handleSearchSuggestionsQuery(SQLiteDatabase db, Uri uri, String[] projection, String limit, CancellationSignal cancellationSignal)182 public Cursor handleSearchSuggestionsQuery(SQLiteDatabase db, Uri uri, String[] projection, 183 String limit, CancellationSignal cancellationSignal) { 184 final MatrixCursor cursor = new MatrixCursor( 185 projection == null ? SEARCH_SUGGESTIONS_COLUMNS : projection); 186 187 if (uri.getPathSegments().size() <= 1) { 188 // no search term, return empty 189 } else { 190 String selection = null; 191 String searchClause = uri.getLastPathSegment(); 192 addSearchSuggestionsBasedOnFilter( 193 cursor, db, projection, selection, searchClause, limit, cancellationSignal); 194 } 195 196 return cursor; 197 } 198 199 /** 200 * Returns a search suggestions cursor for the contact bearing the provided lookup key. If the 201 * lookup key cannot be found in the database, the contact name is decoded from the lookup key 202 * and used to re-identify the contact. If the contact still cannot be found, an empty cursor 203 * is returned. 204 * 205 * <p>Note that if {@code lookupKey} is not a valid lookup key, an empty cursor is returned 206 * silently. This would occur with old-style shortcuts that were created using the contact id 207 * instead of the lookup key. 208 */ handleSearchShortcutRefresh(SQLiteDatabase db, String[] projection, String lookupKey, String filter, CancellationSignal cancellationSignal)209 public Cursor handleSearchShortcutRefresh(SQLiteDatabase db, String[] projection, 210 String lookupKey, String filter, CancellationSignal cancellationSignal) { 211 long contactId; 212 try { 213 contactId = mContactsProvider.lookupContactIdByLookupKey(db, lookupKey); 214 } catch (IllegalArgumentException e) { 215 contactId = -1L; 216 } 217 MatrixCursor cursor = new MatrixCursor( 218 projection == null ? SEARCH_SUGGESTIONS_COLUMNS : projection); 219 return addSearchSuggestionsBasedOnFilter(cursor, 220 db, projection, ContactsColumns.CONCRETE_ID + "=" + contactId, filter, null, 221 cancellationSignal); 222 } 223 addSearchSuggestionsBasedOnFilter(MatrixCursor cursor, SQLiteDatabase db, String[] projection, String selection, String filter, String limit, CancellationSignal cancellationSignal)224 private Cursor addSearchSuggestionsBasedOnFilter(MatrixCursor cursor, SQLiteDatabase db, 225 String[] projection, String selection, String filter, String limit, 226 CancellationSignal cancellationSignal) { 227 StringBuilder sb = new StringBuilder(); 228 final boolean haveFilter = !TextUtils.isEmpty(filter); 229 sb.append("SELECT " 230 + Contacts._ID + ", " 231 + Contacts.LOOKUP_KEY + ", " 232 + Contacts.PHOTO_THUMBNAIL_URI + ", " 233 + Contacts.DISPLAY_NAME + ", " 234 + PRESENCE_SQL + " AS " + Contacts.CONTACT_PRESENCE + ", " 235 + Contacts.LAST_TIME_CONTACTED); 236 if (haveFilter) { 237 sb.append(", " + SearchSnippets.SNIPPET); 238 } 239 sb.append(" FROM "); 240 sb.append(Views.CONTACTS); 241 sb.append(" AS contacts"); 242 if (haveFilter) { 243 mContactsProvider.appendSearchIndexJoin(sb, filter, true, 244 String.valueOf(SNIPPET_START_MATCH), String.valueOf(SNIPPET_END_MATCH), 245 SNIPPET_ELLIPSIS, SNIPPET_MAX_TOKENS, false); 246 } 247 sb.append(" WHERE " + Contacts.LOOKUP_KEY + " IS NOT NULL"); 248 if (selection != null) { 249 sb.append(" AND ").append(selection); 250 } 251 if (limit != null) { 252 sb.append(" LIMIT " + limit); 253 } 254 Cursor c = db.rawQuery(sb.toString(), null, cancellationSignal); 255 SearchSuggestion suggestion = new SearchSuggestion(); 256 suggestion.filter = filter; 257 try { 258 while (c.moveToNext()) { 259 suggestion.contactId = c.getLong(0); 260 suggestion.lookupKey = c.getString(1); 261 suggestion.photoUri = c.getString(2); 262 suggestion.text1 = c.getString(3); 263 suggestion.presence = c.isNull(4) ? -1 : c.getInt(4); 264 suggestion.lastAccessTime = c.getString(5); 265 if (haveFilter) { 266 suggestion.text2 = shortenSnippet(c.getString(6)); 267 } 268 cursor.addRow(suggestion.asList(projection)); 269 suggestion.reset(); 270 } 271 } finally { 272 c.close(); 273 } 274 return cursor; 275 } 276 shortenSnippet(final String snippet)277 private String shortenSnippet(final String snippet) { 278 if (snippet == null) { 279 return null; 280 } 281 282 int from = 0; 283 int to = snippet.length(); 284 int start = snippet.indexOf(SNIPPET_START_MATCH); 285 if (start == -1) { 286 return null; 287 } 288 289 int firstNl = snippet.lastIndexOf('\n', start); 290 if (firstNl != -1) { 291 from = firstNl + 1; 292 } 293 int end = snippet.lastIndexOf(SNIPPET_END_MATCH); 294 if (end != -1) { 295 int lastNl = snippet.indexOf('\n', end); 296 if (lastNl != -1) { 297 to = lastNl; 298 } 299 } 300 301 StringBuilder sb = new StringBuilder(); 302 for (int i = from; i < to; i++) { 303 char c = snippet.charAt(i); 304 if (c != SNIPPET_START_MATCH && c != SNIPPET_END_MATCH) { 305 sb.append(c); 306 } 307 } 308 return sb.toString(); 309 } 310 } 311