| /* |
| * 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 com.android.common.ArrayListCursor; |
| import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns; |
| import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns; |
| import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns; |
| import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns; |
| import com.android.providers.contacts.ContactsDatabaseHelper.Tables; |
| |
| import android.app.SearchManager; |
| import android.content.ContentUris; |
| import android.content.res.Resources; |
| import android.database.Cursor; |
| import android.database.sqlite.SQLiteDatabase; |
| import android.net.Uri; |
| import android.provider.Contacts.Intents; |
| import android.provider.ContactsContract.Contacts; |
| import android.provider.ContactsContract.Data; |
| import android.provider.ContactsContract.RawContacts; |
| import android.provider.ContactsContract.StatusUpdates; |
| import android.provider.ContactsContract.CommonDataKinds.Email; |
| import android.provider.ContactsContract.CommonDataKinds.Organization; |
| import android.provider.ContactsContract.CommonDataKinds.Phone; |
| import android.provider.ContactsContract.CommonDataKinds.StructuredName; |
| import android.provider.ContactsContract.Contacts.Photo; |
| import android.text.TextUtils; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.HashMap; |
| |
| /** |
| * Support for global search integration for Contacts. |
| */ |
| public class GlobalSearchSupport { |
| |
| private static final String[] SEARCH_SUGGESTIONS_BASED_ON_PHONE_NUMBER_COLUMNS = { |
| "_id", |
| SearchManager.SUGGEST_COLUMN_TEXT_1, |
| SearchManager.SUGGEST_COLUMN_TEXT_2, |
| SearchManager.SUGGEST_COLUMN_ICON_1, |
| SearchManager.SUGGEST_COLUMN_INTENT_DATA, |
| SearchManager.SUGGEST_COLUMN_INTENT_ACTION, |
| SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, |
| }; |
| |
| private static final String[] SEARCH_SUGGESTIONS_BASED_ON_NAME_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_ID, |
| SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, |
| }; |
| |
| private interface SearchSuggestionQuery { |
| public static final String TABLE = "data " |
| + " JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) " |
| + " JOIN contacts ON (raw_contacts.contact_id = contacts._id)" |
| + " JOIN " + Tables.RAW_CONTACTS + " AS name_raw_contact ON (" |
| + Contacts.NAME_RAW_CONTACT_ID + "=name_raw_contact." + RawContacts._ID + ")"; |
| |
| public static final String PRESENCE_SQL = |
| "(SELECT " + StatusUpdates.PRESENCE_STATUS + |
| " FROM " + Tables.AGGREGATED_PRESENCE + |
| " WHERE " + AggregatedPresenceColumns.CONTACT_ID |
| + "=" + ContactsColumns.CONCRETE_ID + ")"; |
| |
| public static final String[] COLUMNS = { |
| ContactsColumns.CONCRETE_ID + " AS " + Contacts._ID, |
| "name_raw_contact." + RawContactsColumns.DISPLAY_NAME |
| + " AS " + Contacts.DISPLAY_NAME, |
| PRESENCE_SQL + " AS " + Contacts.CONTACT_PRESENCE, |
| DataColumns.CONCRETE_ID + " AS data_id", |
| DataColumns.MIMETYPE_ID, |
| Data.IS_SUPER_PRIMARY, |
| Data.DATA1, |
| Contacts.PHOTO_ID, |
| Contacts.LOOKUP_KEY, |
| }; |
| |
| public static final int CONTACT_ID = 0; |
| public static final int DISPLAY_NAME = 1; |
| public static final int PRESENCE_STATUS = 2; |
| public static final int DATA_ID = 3; |
| public static final int MIMETYPE_ID = 4; |
| public static final int IS_SUPER_PRIMARY = 5; |
| public static final int ORGANIZATION = 6; |
| public static final int EMAIL = 6; |
| public static final int PHONE = 6; |
| public static final int PHOTO_ID = 7; |
| public static final int LOOKUP_KEY = 8; |
| } |
| |
| private static class SearchSuggestion { |
| long contactId; |
| boolean titleIsName; |
| String organization; |
| String email; |
| String phoneNumber; |
| Uri photoUri; |
| String lookupKey; |
| String normalizedName; |
| int presence = -1; |
| boolean processed; |
| String text1; |
| String text2; |
| String icon1; |
| String icon2; |
| |
| public SearchSuggestion(long contactId) { |
| this.contactId = contactId; |
| } |
| |
| private void process() { |
| if (processed) { |
| return; |
| } |
| |
| boolean hasOrganization = !TextUtils.isEmpty(organization); |
| boolean hasEmail = !TextUtils.isEmpty(email); |
| boolean hasPhone = !TextUtils.isEmpty(phoneNumber); |
| |
| boolean titleIsOrganization = !titleIsName && hasOrganization; |
| boolean titleIsEmail = !titleIsName && !titleIsOrganization && hasEmail; |
| boolean titleIsPhone = !titleIsName && !titleIsOrganization && !titleIsEmail |
| && hasPhone; |
| |
| if (!titleIsOrganization && hasOrganization) { |
| text2 = organization; |
| } else if (!titleIsPhone && hasPhone) { |
| text2 = phoneNumber; |
| } else if (!titleIsEmail && hasEmail) { |
| text2 = email; |
| } |
| |
| 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)); |
| } |
| |
| processed = true; |
| } |
| |
| /** |
| * Returns key for sorting search suggestions. |
| * |
| * <p>TODO: switch to new sort key |
| */ |
| public String getSortKey() { |
| if (normalizedName == null) { |
| process(); |
| normalizedName = text1 == null ? "" : NameNormalizer.normalize(text1); |
| } |
| return normalizedName; |
| } |
| |
| @SuppressWarnings({"unchecked"}) |
| public ArrayList asList(String[] projection) { |
| process(); |
| |
| ArrayList<Object> list = new ArrayList<Object>(); |
| if (projection == null) { |
| list.add(contactId); |
| list.add(text1); |
| list.add(text2); |
| list.add(icon1); |
| list.add(icon2); |
| list.add(lookupKey); |
| list.add(lookupKey); |
| } 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_ID.equals(column)) { |
| list.add(lookupKey); |
| } else if (SearchManager.SUGGEST_COLUMN_SHORTCUT_ID.equals(column)) { |
| list.add(lookupKey); |
| } else { |
| throw new IllegalArgumentException("Invalid column name: " + column); |
| } |
| } |
| } |
| |
| private final ContactsProvider2 mContactsProvider; |
| private boolean mMimeTypeIdsLoaded; |
| private long mMimeTypeIdEmail; |
| private long mMimeTypeIdStructuredName; |
| private long mMimeTypeIdOrganization; |
| private long mMimeTypeIdPhone; |
| |
| @SuppressWarnings("all") |
| public GlobalSearchSupport(ContactsProvider2 contactsProvider) { |
| mContactsProvider = contactsProvider; |
| |
| // 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"); |
| } |
| } |
| |
| private void ensureMimetypeIdsLoaded() { |
| if (!mMimeTypeIdsLoaded) { |
| ContactsDatabaseHelper dbHelper = (ContactsDatabaseHelper)mContactsProvider |
| .getDatabaseHelper(); |
| mMimeTypeIdStructuredName = dbHelper.getMimeTypeId(StructuredName.CONTENT_ITEM_TYPE); |
| mMimeTypeIdOrganization = dbHelper.getMimeTypeId(Organization.CONTENT_ITEM_TYPE); |
| mMimeTypeIdPhone = dbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE); |
| mMimeTypeIdEmail = dbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE); |
| mMimeTypeIdsLoaded = true; |
| } |
| } |
| |
| public Cursor handleSearchSuggestionsQuery(SQLiteDatabase db, Uri uri, String limit) { |
| if (uri.getPathSegments().size() <= 1) { |
| return null; |
| } |
| |
| final String searchClause = uri.getLastPathSegment(); |
| if (TextUtils.isDigitsOnly(searchClause)) { |
| return buildCursorForSearchSuggestionsBasedOnPhoneNumber(searchClause); |
| } else { |
| return buildCursorForSearchSuggestionsBasedOnName(db, searchClause, limit); |
| } |
| } |
| |
| /** |
| * 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 lookupKey, |
| String[] projection) { |
| ensureMimetypeIdsLoaded(); |
| long contactId; |
| try { |
| contactId = mContactsProvider.lookupContactIdByLookupKey(db, lookupKey); |
| } catch (IllegalArgumentException e) { |
| contactId = -1L; |
| } |
| StringBuilder sb = new StringBuilder(); |
| sb.append(mContactsProvider.getContactsRestrictions()); |
| appendMimeTypeFilter(sb); |
| sb.append(" AND " + ContactsColumns.CONCRETE_ID + "=" + contactId); |
| return buildCursorForSearchSuggestions(db, sb.toString(), projection, null); |
| } |
| |
| private Cursor buildCursorForSearchSuggestionsBasedOnPhoneNumber(String searchClause) { |
| Resources r = mContactsProvider.getContext().getResources(); |
| String s; |
| int i; |
| |
| ArrayList<Object> dialNumber = new ArrayList<Object>(); |
| dialNumber.add(0); // _id |
| s = r.getString(com.android.internal.R.string.dial_number_using, searchClause); |
| i = s.indexOf('\n'); |
| if (i < 0) { |
| dialNumber.add(s); |
| dialNumber.add(""); |
| } else { |
| dialNumber.add(s.substring(0, i)); |
| dialNumber.add(s.substring(i + 1)); |
| } |
| dialNumber.add(String.valueOf(com.android.internal.R.drawable.call_contact)); |
| dialNumber.add("tel:" + searchClause); |
| dialNumber.add(Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED); |
| dialNumber.add(null); |
| |
| ArrayList<Object> createContact = new ArrayList<Object>(); |
| createContact.add(1); // _id |
| s = r.getString(com.android.internal.R.string.create_contact_using, searchClause); |
| i = s.indexOf('\n'); |
| if (i < 0) { |
| createContact.add(s); |
| createContact.add(""); |
| } else { |
| createContact.add(s.substring(0, i)); |
| createContact.add(s.substring(i + 1)); |
| } |
| createContact.add(String.valueOf(com.android.internal.R.drawable.create_contact)); |
| createContact.add("tel:" + searchClause); |
| createContact.add(Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED); |
| createContact.add(SearchManager.SUGGEST_NEVER_MAKE_SHORTCUT); |
| |
| @SuppressWarnings({"unchecked"}) ArrayList<ArrayList> rows = new ArrayList<ArrayList>(); |
| rows.add(dialNumber); |
| rows.add(createContact); |
| |
| return new ArrayListCursor(SEARCH_SUGGESTIONS_BASED_ON_PHONE_NUMBER_COLUMNS, rows); |
| } |
| |
| private Cursor buildCursorForSearchSuggestionsBasedOnName(SQLiteDatabase db, |
| String searchClause, String limit) { |
| ensureMimetypeIdsLoaded(); |
| StringBuilder sb = new StringBuilder(); |
| sb.append(mContactsProvider.getContactsRestrictions()); |
| appendMimeTypeFilter(sb); |
| sb.append(" AND " + DataColumns.CONCRETE_RAW_CONTACT_ID + " IN "); |
| mContactsProvider.appendRawContactsByFilterAsNestedQuery(sb, searchClause); |
| |
| /* |
| * Prepending "+" to the IN_VISIBLE_GROUP column disables the index on the |
| * that column. The logic is this: let's say we have 10,000 contacts |
| * of which 500 are visible. The first letter we type narrows this down |
| * to 10,000/26 = 384, which is already less than 500 that we would get |
| * from the IN_VISIBLE_GROUP index. Typing the second letter will narrow |
| * the search down to 10,000/26/26 = 14 contacts. And a lot of people |
| * will have more that 5% of their contacts visible, while the alphabet |
| * will always have 26 letters. |
| */ |
| sb.append(" AND " + "+" + Contacts.IN_VISIBLE_GROUP + "=1"); |
| String selection = sb.toString(); |
| |
| return buildCursorForSearchSuggestions(db, selection, null, limit); |
| } |
| |
| private void appendMimeTypeFilter(StringBuilder sb) { |
| |
| /* |
| * The "+" syntax prevents the mime type index from being used - we just want |
| * to reduce the size of the result set, not actually search by mime types. |
| */ |
| sb.append(" AND " + "+" + DataColumns.MIMETYPE_ID + " IN (" + mMimeTypeIdEmail + "," + |
| mMimeTypeIdOrganization + "," + mMimeTypeIdPhone + "," + |
| mMimeTypeIdStructuredName + ")"); |
| } |
| |
| private Cursor buildCursorForSearchSuggestions(SQLiteDatabase db, |
| String selection, String[] projection, String limit) { |
| ArrayList<SearchSuggestion> suggestionList = new ArrayList<SearchSuggestion>(); |
| HashMap<Long, SearchSuggestion> suggestionMap = new HashMap<Long, SearchSuggestion>(); |
| Cursor c = db.query(false, SearchSuggestionQuery.TABLE, |
| SearchSuggestionQuery.COLUMNS, selection, null, null, null, null, limit); |
| try { |
| while (c.moveToNext()) { |
| |
| long contactId = c.getLong(SearchSuggestionQuery.CONTACT_ID); |
| SearchSuggestion suggestion = suggestionMap.get(contactId); |
| if (suggestion == null) { |
| suggestion = new SearchSuggestion(contactId); |
| suggestionList.add(suggestion); |
| suggestionMap.put(contactId, suggestion); |
| } |
| |
| boolean isSuperPrimary = c.getInt(SearchSuggestionQuery.IS_SUPER_PRIMARY) != 0; |
| suggestion.text1 = c.getString(SearchSuggestionQuery.DISPLAY_NAME); |
| |
| if (!c.isNull(SearchSuggestionQuery.PRESENCE_STATUS)) { |
| suggestion.presence = c.getInt(SearchSuggestionQuery.PRESENCE_STATUS); |
| } |
| |
| long mimetype = c.getLong(SearchSuggestionQuery.MIMETYPE_ID); |
| if (mimetype == mMimeTypeIdStructuredName) { |
| suggestion.titleIsName = true; |
| } else if (mimetype == mMimeTypeIdOrganization) { |
| if (isSuperPrimary || suggestion.organization == null) { |
| suggestion.organization = c.getString(SearchSuggestionQuery.ORGANIZATION); |
| } |
| } else if (mimetype == mMimeTypeIdEmail) { |
| if (isSuperPrimary || suggestion.email == null) { |
| suggestion.email = c.getString(SearchSuggestionQuery.EMAIL); |
| } |
| } else if (mimetype == mMimeTypeIdPhone) { |
| if (isSuperPrimary || suggestion.phoneNumber == null) { |
| suggestion.phoneNumber = c.getString(SearchSuggestionQuery.PHONE); |
| } |
| } |
| |
| if (!c.isNull(SearchSuggestionQuery.PHOTO_ID)) { |
| suggestion.photoUri = Uri.withAppendedPath( |
| ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId), |
| Photo.CONTENT_DIRECTORY); |
| } |
| |
| suggestion.lookupKey = c.getString(SearchSuggestionQuery.LOOKUP_KEY); |
| } |
| } finally { |
| c.close(); |
| } |
| |
| Collections.sort(suggestionList, new Comparator<SearchSuggestion>() { |
| public int compare(SearchSuggestion row1, SearchSuggestion row2) { |
| return row1.getSortKey().compareTo(row2.getSortKey()); |
| } |
| }); |
| |
| @SuppressWarnings({"unchecked"}) ArrayList<ArrayList> rows = new ArrayList<ArrayList>(); |
| for (int i = 0; i < suggestionList.size(); i++) { |
| rows.add(suggestionList.get(i).asList(projection)); |
| } |
| |
| return new ArrayListCursor(projection != null ? projection |
| : SEARCH_SUGGESTIONS_BASED_ON_NAME_COLUMNS, rows); |
| } |
| } |