| /* |
| * Copyright (C) 2009 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License |
| */ |
| |
| package com.android.providers.contacts; |
| |
| import android.app.SearchManager; |
| import android.content.Context; |
| import android.database.Cursor; |
| import android.database.MatrixCursor; |
| import android.database.sqlite.SQLiteDatabase; |
| import android.net.Uri; |
| import android.os.CancellationSignal; |
| import android.provider.ContactsContract.CommonDataKinds.Email; |
| import android.provider.ContactsContract.CommonDataKinds.Organization; |
| import android.provider.ContactsContract.CommonDataKinds.Phone; |
| import android.provider.ContactsContract.Contacts; |
| import android.provider.ContactsContract.Data; |
| import android.provider.ContactsContract.SearchSnippets; |
| import android.provider.ContactsContract.StatusUpdates; |
| import android.telephony.TelephonyManager; |
| import android.text.TextUtils; |
| |
| import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns; |
| import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns; |
| import com.android.providers.contacts.ContactsDatabaseHelper.Tables; |
| import com.android.providers.contacts.ContactsDatabaseHelper.Views; |
| |
| import java.util.ArrayList; |
| |
| /** |
| * Support for global search integration for Contacts. |
| */ |
| public class GlobalSearchSupport { |
| |
| private static final String[] SEARCH_SUGGESTIONS_COLUMNS = { |
| "_id", |
| SearchManager.SUGGEST_COLUMN_TEXT_1, |
| SearchManager.SUGGEST_COLUMN_TEXT_2, |
| SearchManager.SUGGEST_COLUMN_ICON_1, |
| SearchManager.SUGGEST_COLUMN_ICON_2, |
| SearchManager.SUGGEST_COLUMN_INTENT_DATA, |
| SearchManager.SUGGEST_COLUMN_INTENT_ACTION, |
| SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, |
| SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA, |
| SearchManager.SUGGEST_COLUMN_LAST_ACCESS_HINT, |
| }; |
| |
| private static final char SNIPPET_START_MATCH = '\u0001'; |
| private static final char SNIPPET_END_MATCH = '\u0001'; |
| private static final String SNIPPET_ELLIPSIS = "\u2026"; |
| private static final int SNIPPET_MAX_TOKENS = 5; |
| |
| private static final String PRESENCE_SQL = |
| "(SELECT " + StatusUpdates.PRESENCE + |
| " FROM " + Tables.AGGREGATED_PRESENCE + |
| " WHERE " + AggregatedPresenceColumns.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + ")"; |
| |
| private static class SearchSuggestion { |
| long contactId; |
| String photoUri; |
| String lookupKey; |
| int presence = -1; |
| String text1; |
| String text2; |
| String icon1; |
| String icon2; |
| String intentData; |
| String intentAction; |
| String filter; |
| String lastAccessTime; |
| |
| @SuppressWarnings({"unchecked"}) |
| public ArrayList<?> asList(String[] projection) { |
| if (icon1 == null) { |
| if (photoUri != null) { |
| icon1 = photoUri.toString(); |
| } else { |
| icon1 = String.valueOf(com.android.internal.R.drawable.ic_contact_picture); |
| } |
| } |
| |
| if (presence != -1) { |
| icon2 = String.valueOf(StatusUpdates.getPresenceIconResourceId(presence)); |
| } |
| |
| ArrayList<Object> list = new ArrayList<Object>(); |
| if (projection == null) { |
| list.add(contactId); // _id |
| list.add(text1); // text1 |
| list.add(text2); // text2 |
| list.add(icon1); // icon1 |
| list.add(icon2); // icon2 |
| list.add(intentData == null ? buildUri() : intentData); // intent data |
| list.add(intentAction); // intentAction |
| list.add(lookupKey); // shortcut id |
| list.add(filter); // extra data |
| list.add(lastAccessTime); // last access hint |
| } else { |
| for (int i = 0; i < projection.length; i++) { |
| addColumnValue(list, projection[i]); |
| } |
| } |
| return list; |
| } |
| |
| private void addColumnValue(ArrayList<Object> list, String column) { |
| if ("_id".equals(column)) { |
| list.add(contactId); |
| } else if (SearchManager.SUGGEST_COLUMN_TEXT_1.equals(column)) { |
| list.add(text1); |
| } else if (SearchManager.SUGGEST_COLUMN_TEXT_2.equals(column)) { |
| list.add(text2); |
| } else if (SearchManager.SUGGEST_COLUMN_ICON_1.equals(column)) { |
| list.add(icon1); |
| } else if (SearchManager.SUGGEST_COLUMN_ICON_2.equals(column)) { |
| list.add(icon2); |
| } else if (SearchManager.SUGGEST_COLUMN_INTENT_DATA.equals(column)) { |
| list.add(intentData == null ? buildUri() : intentData); |
| } else if (SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID.equals(column)) { |
| list.add(lookupKey); |
| } else if (SearchManager.SUGGEST_COLUMN_SHORTCUT_ID.equals(column)) { |
| list.add(lookupKey); |
| } else if (SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA.equals(column)) { |
| list.add(filter); |
| } else if (SearchManager.SUGGEST_COLUMN_LAST_ACCESS_HINT.equals(column)) { |
| list.add(lastAccessTime); |
| } else { |
| throw new IllegalArgumentException("Invalid column name: " + column); |
| } |
| } |
| |
| private String buildUri() { |
| return Contacts.getLookupUri(contactId, lookupKey).toString(); |
| } |
| |
| public void reset() { |
| contactId = 0; |
| photoUri = null; |
| lookupKey = null; |
| presence = -1; |
| text1 = null; |
| text2 = null; |
| icon1 = null; |
| icon2 = null; |
| intentData = null; |
| intentAction = null; |
| filter = null; |
| lastAccessTime = null; |
| } |
| } |
| |
| private final ContactsProvider2 mContactsProvider; |
| |
| @SuppressWarnings("all") |
| public GlobalSearchSupport(ContactsProvider2 contactsProvider) { |
| mContactsProvider = contactsProvider; |
| |
| TelephonyManager telman = (TelephonyManager) |
| mContactsProvider.getContext().getSystemService(Context.TELEPHONY_SERVICE); |
| |
| // To ensure the data column position. This is dead code if properly configured. |
| if (Organization.COMPANY != Data.DATA1 || Phone.NUMBER != Data.DATA1 |
| || Email.DATA != Data.DATA1) { |
| throw new AssertionError("Some of ContactsContract.CommonDataKinds class primary" |
| + " data is not in DATA1 column"); |
| } |
| } |
| |
| public Cursor handleSearchSuggestionsQuery(SQLiteDatabase db, Uri uri, String[] projection, |
| String limit, CancellationSignal cancellationSignal) { |
| final MatrixCursor cursor = new MatrixCursor( |
| projection == null ? SEARCH_SUGGESTIONS_COLUMNS : projection); |
| |
| if (uri.getPathSegments().size() <= 1) { |
| // no search term, return empty |
| } else { |
| String selection = null; |
| String searchClause = uri.getLastPathSegment(); |
| addSearchSuggestionsBasedOnFilter( |
| cursor, db, projection, selection, searchClause, limit, cancellationSignal); |
| } |
| |
| return cursor; |
| } |
| |
| /** |
| * Returns a search suggestions cursor for the contact bearing the provided lookup key. If the |
| * lookup key cannot be found in the database, the contact name is decoded from the lookup key |
| * and used to re-identify the contact. If the contact still cannot be found, an empty cursor |
| * is returned. |
| * |
| * <p>Note that if {@code lookupKey} is not a valid lookup key, an empty cursor is returned |
| * silently. This would occur with old-style shortcuts that were created using the contact id |
| * instead of the lookup key. |
| */ |
| public Cursor handleSearchShortcutRefresh(SQLiteDatabase db, String[] projection, |
| String lookupKey, String filter, CancellationSignal cancellationSignal) { |
| long contactId; |
| try { |
| contactId = mContactsProvider.lookupContactIdByLookupKey(db, lookupKey); |
| } catch (IllegalArgumentException e) { |
| contactId = -1L; |
| } |
| MatrixCursor cursor = new MatrixCursor( |
| projection == null ? SEARCH_SUGGESTIONS_COLUMNS : projection); |
| return addSearchSuggestionsBasedOnFilter(cursor, |
| db, projection, ContactsColumns.CONCRETE_ID + "=" + contactId, filter, null, |
| cancellationSignal); |
| } |
| |
| private Cursor addSearchSuggestionsBasedOnFilter(MatrixCursor cursor, SQLiteDatabase db, |
| String[] projection, String selection, String filter, String limit, |
| CancellationSignal cancellationSignal) { |
| StringBuilder sb = new StringBuilder(); |
| final boolean haveFilter = !TextUtils.isEmpty(filter); |
| sb.append("SELECT " |
| + Contacts._ID + ", " |
| + Contacts.LOOKUP_KEY + ", " |
| + Contacts.PHOTO_THUMBNAIL_URI + ", " |
| + Contacts.DISPLAY_NAME + ", " |
| + PRESENCE_SQL + " AS " + Contacts.CONTACT_PRESENCE + ", " |
| + Contacts.LR_LAST_TIME_CONTACTED); |
| if (haveFilter) { |
| sb.append(", " + SearchSnippets.SNIPPET); |
| } |
| sb.append(" FROM "); |
| sb.append(Views.CONTACTS); |
| sb.append(" AS contacts"); |
| if (haveFilter) { |
| mContactsProvider.appendSearchIndexJoin(sb, filter, true, |
| String.valueOf(SNIPPET_START_MATCH), String.valueOf(SNIPPET_END_MATCH), |
| SNIPPET_ELLIPSIS, SNIPPET_MAX_TOKENS, false); |
| } |
| sb.append(" WHERE " + Contacts.LOOKUP_KEY + " IS NOT NULL"); |
| if (selection != null) { |
| sb.append(" AND ").append(selection); |
| } |
| if (limit != null) { |
| sb.append(" LIMIT " + limit); |
| } |
| Cursor c = db.rawQuery(sb.toString(), null, cancellationSignal); |
| SearchSuggestion suggestion = new SearchSuggestion(); |
| suggestion.filter = filter; |
| try { |
| while (c.moveToNext()) { |
| suggestion.contactId = c.getLong(0); |
| suggestion.lookupKey = c.getString(1); |
| suggestion.photoUri = c.getString(2); |
| suggestion.text1 = c.getString(3); |
| suggestion.presence = c.isNull(4) ? -1 : c.getInt(4); |
| suggestion.lastAccessTime = c.getString(5); |
| if (haveFilter) { |
| suggestion.text2 = shortenSnippet(c.getString(6)); |
| } |
| cursor.addRow(suggestion.asList(projection)); |
| suggestion.reset(); |
| } |
| } finally { |
| c.close(); |
| } |
| return cursor; |
| } |
| |
| private String shortenSnippet(final String snippet) { |
| if (snippet == null) { |
| return null; |
| } |
| |
| int from = 0; |
| int to = snippet.length(); |
| int start = snippet.indexOf(SNIPPET_START_MATCH); |
| if (start == -1) { |
| return null; |
| } |
| |
| int firstNl = snippet.lastIndexOf('\n', start); |
| if (firstNl != -1) { |
| from = firstNl + 1; |
| } |
| int end = snippet.lastIndexOf(SNIPPET_END_MATCH); |
| if (end != -1) { |
| int lastNl = snippet.indexOf('\n', end); |
| if (lastNl != -1) { |
| to = lastNl; |
| } |
| } |
| |
| StringBuilder sb = new StringBuilder(); |
| for (int i = from; i < to; i++) { |
| char c = snippet.charAt(i); |
| if (c != SNIPPET_START_MATCH && c != SNIPPET_END_MATCH) { |
| sb.append(c); |
| } |
| } |
| return sb.toString(); |
| } |
| } |