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