/* * 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.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.MatrixCursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.provider.ContactsContract; 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; import android.provider.ContactsContract.Contacts.Photo; import android.provider.ContactsContract.Data; import android.provider.ContactsContract.RawContacts; import android.provider.ContactsContract.StatusUpdates; 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 default_directory on (raw_contacts.contact_id = default_directory._id) " + " JOIN contacts ON (default_directory._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 + " 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; // Current contacts - those contacted within the last 3 days (in seconds) public static final long CURRENT_CONTACTS = 3 * 24 * 60 * 60; // Recent contacts - those contacted within the last 30 days (in seconds) public static final long RECENT_CONTACTS = 30 * 24 * 60 * 60; public static final String TIME_SINCE_LAST_CONTACTED = "(strftime('%s', 'now') - contacts." + Contacts.LAST_TIME_CONTACTED + "/1000)"; /* * See {@link ContactsProvider2#EMAIL_FILTER_SORT_ORDER} for the discussion of this * sorting order. */ public static final String SORT_ORDER = "(CASE WHEN contacts." + Contacts.STARRED + "=1 THEN 0 ELSE 1 END), " + "(CASE WHEN " + TIME_SINCE_LAST_CONTACTED + " < " + CURRENT_CONTACTS + " THEN 0 " + " WHEN " + TIME_SINCE_LAST_CONTACTED + " < " + RECENT_CONTACTS + " THEN 1 " + " ELSE 2 END)," + "contacts." + Contacts.TIMES_CONTACTED + " DESC, " + "name_raw_contact." + RawContacts.DISPLAY_NAME_PRIMARY + ", " + "contacts." + Contacts._ID; } 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. * *

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 list = new ArrayList(); 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 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. * *

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) { MatrixCursor cursor = new MatrixCursor(SEARCH_SUGGESTIONS_BASED_ON_PHONE_NUMBER_COLUMNS); Resources r = mContactsProvider.getContext().getResources(); String s; int i; ArrayList dialNumber = new ArrayList(); 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(ContactsContract.Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED); dialNumber.add(null); cursor.addRow(dialNumber); ArrayList createContact = new ArrayList(); 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(ContactsContract.Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED); createContact.add(SearchManager.SUGGEST_NEVER_MAKE_SHORTCUT); cursor.addRow(createContact); return cursor; } 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); 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 suggestionList = new ArrayList(); HashMap suggestionMap = new HashMap(); Cursor c = db.query(false, SearchSuggestionQuery.TABLE, SearchSuggestionQuery.COLUMNS, selection, null, null, null, SearchSuggestionQuery.SORT_ORDER, 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() { public int compare(SearchSuggestion row1, SearchSuggestion row2) { return row1.getSortKey().compareTo(row2.getSortKey()); } }); MatrixCursor cursor = new MatrixCursor(projection != null ? projection : SEARCH_SUGGESTIONS_BASED_ON_NAME_COLUMNS); for (int i = 0; i < suggestionList.size(); i++) { cursor.addRow(suggestionList.get(i).asList(projection)); } return cursor; } }