Reimplementing global search integration in ContactsProvider2.
Also fixing bugs in the area of logical deletion of raw contacts and their exclusion from further aggregation.
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 77b5fd3..d7de1fb 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -18,11 +18,12 @@
android:icon="@drawable/app_icon">
<provider android:name="ContactsProvider"
- android:authorities="contacts;call_log"
+ android:authorities="contacts"
android:syncable="false" android:multiprocess="false"
android:readPermission="android.permission.READ_CONTACTS"
android:writePermission="android.permission.WRITE_CONTACTS">
- <path-permission android:path="/people/search_suggest_query"
+ <path-permission
+ android:path="/people/search_suggest_query"
android:readPermission="android.permission.GLOBAL_SEARCH" />
</provider>
@@ -31,7 +32,18 @@
android:syncable="false"
android:multiprocess="false"
android:readPermission="android.permission.READ_CONTACTS"
- android:writePermission="android.permission.WRITE_CONTACTS" />
+ android:writePermission="android.permission.WRITE_CONTACTS">
+ <path-permission
+ android:path="/contacts/search_suggest_query"
+ android:readPermission="android.permission.GLOBAL_SEARCH" />
+ </provider>
+
+ <provider android:name="CallLogProvider"
+ android:authorities="call_log"
+ android:syncable="false" android:multiprocess="false"
+ android:readPermission="android.permission.READ_CONTACTS"
+ android:writePermission="android.permission.WRITE_CONTACTS">
+ </provider>
<!-- TODO: create permissions for social data -->
<provider android:name="SocialProvider"
diff --git a/src/com/android/providers/contacts/ContactAggregator.java b/src/com/android/providers/contacts/ContactAggregator.java
index 637601c..ad70640 100644
--- a/src/com/android/providers/contacts/ContactAggregator.java
+++ b/src/com/android/providers/contacts/ContactAggregator.java
@@ -307,30 +307,31 @@
public int markContactForAggregation(long rawContactId) {
final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
- long contactId = mOpenHelper.getContactId(rawContactId);
- if (contactId != 0) {
-
- // Clear out the contact ID field on the contact
- ContentValues values = new ContentValues();
- values.putNull(RawContacts.CONTACT_ID);
- int updated = db.update(Tables.RAW_CONTACTS, values,
- RawContacts._ID + "=" + rawContactId + " AND " + RawContacts.AGGREGATION_MODE + "="
- + RawContacts.AGGREGATION_MODE_DEFAULT, null);
- if (updated == 0) {
- return mOpenHelper.getAggregationMode(rawContactId);
- }
-
- // Clear out data used for aggregation - we will recreate it during aggregation
- db.execSQL("DELETE FROM " + Tables.NAME_LOOKUP + " WHERE "
- + NameLookupColumns.RAW_CONTACT_ID + "=" + rawContactId);
-
- // Delete the aggregate contact itself if it no longer has constituent raw contacts
- db.execSQL("DELETE FROM " + Tables.CONTACTS + " WHERE " + Contacts._ID + "="
- + contactId + " AND " + Contacts._ID + " NOT IN (SELECT "
- + RawContacts.CONTACT_ID + " FROM " + Tables.RAW_CONTACTS + ");");
- return RawContacts.AGGREGATION_MODE_DEFAULT;
+ int aggregationMode = mOpenHelper.getAggregationMode(rawContactId);
+ if (aggregationMode == RawContacts.AGGREGATION_MODE_DISABLED) {
+ return aggregationMode;
}
- return RawContacts.AGGREGATION_MODE_DISABLED;
+
+ long contactId = mOpenHelper.getContactId(rawContactId);
+ if (contactId == 0) {
+ // Not aggregated
+ return aggregationMode;
+ }
+
+ mOpenHelper.removeContactIfSingleton(rawContactId);
+
+ // TODO compiled statements
+
+ // Clear out data used for aggregation - we will recreate it during aggregation
+ db.execSQL("DELETE FROM " + Tables.NAME_LOOKUP + " WHERE "
+ + NameLookupColumns.RAW_CONTACT_ID + "=" + rawContactId);
+
+ // Clear out the contact ID field on the contact
+ ContentValues values = new ContentValues();
+ values.putNull(RawContacts.CONTACT_ID);
+ db.update(Tables.RAW_CONTACTS, values, RawContacts._ID + "=" + rawContactId, null);
+
+ return aggregationMode;
}
public void updateAggregateData(long contactId) {
@@ -462,11 +463,8 @@
}
selection.append(secondaryContactIds.get(i));
}
- selection.append(") AND ")
- .append(MimetypesColumns.MIMETYPE)
- .append("='")
- .append(StructuredName.CONTENT_ITEM_TYPE)
- .append("'");
+ selection.append(") AND " + MimetypesColumns.MIMETYPE + "='"
+ + StructuredName.CONTENT_ITEM_TYPE + "'");
final Cursor c = db.query(Tables.DATA_JOIN_MIMETYPE_RAW_CONTACTS,
DATA_JOIN_MIMETYPE_AND_CONTACT_COLUMNS,
@@ -516,8 +514,8 @@
final Cursor c = db.query(Tables.DATA_JOIN_MIMETYPE_RAW_CONTACTS,
DATA_JOIN_MIMETYPE_COLUMNS,
- DatabaseUtils.concatenateWhere(Data.RAW_CONTACT_ID + "=" + rawContactId,
- MIMETYPE_SELECTION_IN_CLAUSE),
+ Data.RAW_CONTACT_ID + "=" + rawContactId + " AND ("
+ + MIMETYPE_SELECTION_IN_CLAUSE + ")",
null, null, null, null);
try {
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index 438e470..242c705 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -17,6 +17,7 @@
package com.android.providers.contacts;
import com.android.internal.content.SyncStateContentProviderHelper;
+import com.android.internal.database.ArrayListCursor;
import com.android.providers.contacts.OpenHelper.AggregationExceptionColumns;
import com.android.providers.contacts.OpenHelper.Clauses;
import com.android.providers.contacts.OpenHelper.ContactsColumns;
@@ -31,6 +32,7 @@
import com.google.android.collect.Lists;
import android.accounts.Account;
+import android.app.SearchManager;
import android.content.ContentProvider;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
@@ -42,6 +44,7 @@
import android.content.OperationApplicationException;
import android.content.UriMatcher;
import android.content.pm.PackageManager;
+import android.content.res.Resources;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteCursor;
@@ -53,7 +56,7 @@
import android.os.RemoteException;
import android.provider.BaseColumns;
import android.provider.ContactsContract;
-import android.provider.Contacts.ContactMethods;
+import android.provider.Contacts.Intents;
import android.provider.ContactsContract.AggregationExceptions;
import android.provider.ContactsContract.CommonDataKinds;
import android.provider.ContactsContract.Contacts;
@@ -68,13 +71,17 @@
import android.provider.ContactsContract.CommonDataKinds.Nickname;
import android.provider.ContactsContract.CommonDataKinds.Organization;
import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
import android.provider.ContactsContract.Contacts.AggregationSuggestions;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
+import android.util.Log;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.HashMap;
/**
@@ -138,6 +145,9 @@
private static final int SYNCSTATE = 11000;
+ private static final int SEARCH_SUGGESTIONS = 12001;
+ private static final int SEARCH_SHORTCUT = 12002;
+
private interface ContactsQuery {
public static final String TABLE = Tables.RAW_CONTACTS;
@@ -338,8 +348,8 @@
/** Precompiled sql statement for updating a contact display name */
private SQLiteStatement mContactDisplayNameUpdate;
- private static final String GTALK_PROTOCOL_STRING = ContactMethods
- .encodePredefinedImProtocol(ContactMethods.PROTOCOL_GOOGLE_TALK);
+ private static final String GTALK_PROTOCOL_STRING = Im
+ .encodePredefinedImProtocol(Im.PROTOCOL_GOOGLE_TALK);
static {
// Contacts URI matching table
@@ -387,6 +397,13 @@
matcher.addURI(ContactsContract.AUTHORITY, "presence", PRESENCE);
matcher.addURI(ContactsContract.AUTHORITY, "presence/#", PRESENCE_ID);
+ matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY,
+ SEARCH_SUGGESTIONS);
+ matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
+ SEARCH_SUGGESTIONS);
+ matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/#",
+ SEARCH_SHORTCUT);
+
HashMap<String, String> columns;
// Contacts projection map
@@ -1146,17 +1163,7 @@
return -1;
}
- long rawContactId = db.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID,
- overriddenValues);
-
- int aggregationMode = RawContacts.AGGREGATION_MODE_DEFAULT;
- if (values.containsKey(RawContacts.AGGREGATION_MODE)) {
- aggregationMode = values.getAsInteger(RawContacts.AGGREGATION_MODE);
- }
-
- triggerAggregation(rawContactId, aggregationMode);
-
- return rawContactId;
+ return db.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, overriddenValues);
}
/**
@@ -1657,12 +1664,16 @@
public int deleteRawContact(long rawContactId, boolean permanently) {
final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ // TODO delete aggregation exceptions
+ mOpenHelper.removeContactIfSingleton(rawContactId);
if (permanently) {
db.delete(Tables.PRESENCE, Presence.RAW_CONTACT_ID + "=" + rawContactId, null);
return db.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null);
} else {
mValues.clear();
mValues.put(RawContacts.DELETED, true);
+ mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
+ mValues.putNull(RawContacts.CONTACT_ID);
return updateRawContact(rawContactId, mValues, null, null);
}
}
@@ -1977,7 +1988,8 @@
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
String groupBy = null;
- String limit = null;
+ String limit = getLimit(uri);
+
String contactIdColName = Tables.CONTACTS + "." + Contacts._ID;
// TODO: Consider writing a test case for RestrictionExceptions when you
@@ -2238,6 +2250,8 @@
case AGGREGATION_SUGGESTIONS: {
long contactId = Long.parseLong(uri.getPathSegments().get(1));
+
+ // TODO drop MAX_SUGGESTIONS in favor of LIMIT
final String maxSuggestionsParam =
uri.getQueryParameter(AggregationSuggestions.MAX_SUGGESTIONS);
@@ -2265,9 +2279,18 @@
break;
}
+ case SEARCH_SUGGESTIONS: {
+ return handleSearchSuggestionsQuery(uri, limit);
+ }
+
+ case SEARCH_SHORTCUT: {
+ // TODO
+ break;
+ }
+
default:
return mLegacyApiSupport.query(uri, projection, selection, selectionArgs,
- sortOrder);
+ sortOrder, limit);
}
// Perform the query and set the notification uri
@@ -2280,6 +2303,304 @@
}
/**
+ * Gets the value of the "limit" URI query parameter.
+ *
+ * @return A string containing a non-negative integer, or <code>null</code> if
+ * the parameter is not set, or is set to an invalid value.
+ */
+ private String getLimit(Uri url) {
+ String limitParam = url.getQueryParameter("limit");
+ if (limitParam == null) {
+ return null;
+ }
+ // make sure that the limit is a non-negative integer
+ try {
+ int l = Integer.parseInt(limitParam);
+ if (l < 0) {
+ Log.w(TAG, "Invalid limit parameter: " + limitParam);
+ return null;
+ }
+ return String.valueOf(l);
+ } catch (NumberFormatException ex) {
+ Log.w(TAG, "Invalid limit parameter: " + limitParam);
+ return null;
+ }
+ }
+
+
+ public Cursor handleSearchSuggestionsQuery(Uri url, String limit) {
+ if (url.getPathSegments().size() <= 1) {
+ return null;
+ }
+
+ final String searchClause = url.getLastPathSegment();
+ if (TextUtils.isDigitsOnly(searchClause)) {
+ return buildCursorForSearchSuggestionsBasedOnPhoneNumber(searchClause);
+ } else {
+ return buildCursorForSearchSuggestionsBasedOnName(searchClause, limit);
+ }
+ }
+
+ 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 Cursor buildCursorForSearchSuggestionsBasedOnPhoneNumber(String searchClause) {
+ Resources r = 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 interface SearchSuggestionQuery {
+ public static final String JOIN_RAW_CONTACTS =
+ " JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) ";
+
+ public static final String JOIN_CONTACTS =
+ " JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
+
+ public static final String JOIN_MIMETYPES =
+ " JOIN mimetypes ON (data.mimetype_id = mimetypes._id AND mimetypes.mimetype IN ('"
+ + StructuredName.CONTENT_ITEM_TYPE + "','" + Email.CONTENT_ITEM_TYPE + "','"
+ + Phone.CONTENT_ITEM_TYPE + "','" + Organization.CONTENT_ITEM_TYPE + "','"
+ + Photo.CONTENT_ITEM_TYPE + "','" + GroupMembership.CONTENT_ITEM_TYPE + "')) ";
+
+ // TODO join with groups and ensure that suggestions are from the My Contacts group
+ public static final String JOIN_GROUPS = " JOIN groups ON (mimetypes.mimetype='"
+ + GroupMembership.CONTENT_ITEM_TYPE + "' " + " AND groups._id = data."
+ + GroupMembership.GROUP_ROW_ID + ") ";
+
+ public static final String TABLE = "data " + JOIN_RAW_CONTACTS + JOIN_MIMETYPES
+ + JOIN_CONTACTS;
+
+ public static final String PRESENCE_SQL = "(SELECT MAX(" + Presence.PRESENCE_STATUS
+ + ") FROM " + Tables.PRESENCE + " WHERE " + Tables.PRESENCE + "."
+ + Presence.RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID + ")";
+
+ public static final String[] COLUMNS = {
+ ContactsColumns.CONCRETE_ID + " AS " + Contacts._ID,
+ ContactsColumns.CONCRETE_DISPLAY_NAME + " AS " + Contacts.DISPLAY_NAME,
+ PRESENCE_SQL + " AS " + Contacts.PRESENCE_STATUS,
+ DataColumns.CONCRETE_ID + " AS data_id",
+ MimetypesColumns.MIMETYPE,
+ Data.IS_SUPER_PRIMARY,
+ Data.DATA2,
+ };
+
+ 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 = 4;
+ public static final int IS_SUPER_PRIMARY = 5;
+ public static final int DATA2 = 6;
+ }
+
+ private static class SearchSuggestion {
+ String contactId;
+ boolean titleIsName;
+ String organization;
+ String email;
+ String phoneNumber;
+ String photoUri;
+ String normalizedName;
+ int presence = -1;
+ boolean processed;
+ String text1;
+ String text2;
+ String icon1;
+ String icon2;
+
+ public SearchSuggestion(long contactId) {
+ this.contactId = String.valueOf(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 (!titleIsEmail && hasEmail) {
+ text2 = email;
+ } else if (!titleIsPhone && hasPhone) {
+ text2 = phoneNumber;
+ }
+
+ if (photoUri != null) {
+ icon1 = photoUri;
+ } else {
+ icon1 = String.valueOf(com.android.internal.R.drawable.ic_contact_picture);
+ }
+
+ if (presence != -1) {
+ icon2 = String.valueOf(Presence.getPresenceIconResourceId(presence));
+ }
+
+ processed = true;
+ }
+
+ public String getSortKey() {
+ if (normalizedName == null) {
+ process();
+ normalizedName = text1 == null ? "" : NameNormalizer.normalize(text1);
+ }
+ return normalizedName;
+ }
+
+ @SuppressWarnings({"unchecked"})
+ public ArrayList asList() {
+ process();
+
+ ArrayList<Object> list = new ArrayList<Object>();
+ list.add(contactId);
+ list.add(text1);
+ list.add(text2);
+ list.add(icon1);
+ list.add(icon2);
+ list.add(contactId);
+ list.add(contactId);
+ return list;
+ }
+ }
+
+ 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 Cursor buildCursorForSearchSuggestionsBasedOnName(String searchClause, String limit) {
+ ArrayList<SearchSuggestion> suggestionList = new ArrayList<SearchSuggestion>();
+ HashMap<Long, SearchSuggestion> suggestionMap = new HashMap<Long, SearchSuggestion>();
+
+ StringBuilder selection = new StringBuilder();
+ selection.append(getContactsRestrictionExceptions());
+ selection.append(" AND " + DataColumns.CONCRETE_RAW_CONTACT_ID + " IN ");
+ appendRawContactsByFilterAsNestedQuery(selection, searchClause, limit);
+
+ SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+ Cursor c = db.query(true, SearchSuggestionQuery.TABLE,
+ SearchSuggestionQuery.COLUMNS, selection.toString(), null, null, null, null, null);
+ 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);
+ }
+
+ String mimetype = c.getString(SearchSuggestionQuery.MIMETYPE);
+ if (StructuredName.CONTENT_ITEM_TYPE.equals(mimetype)) {
+ suggestion.titleIsName = true;
+ } else if (Photo.CONTENT_ITEM_TYPE.equals(mimetype)) {
+ if (isSuperPrimary || suggestion.photoUri == null) {
+
+ // TODO introduce a dedicate URI for contact photo: /contact/#/photo
+ long dataId = c.getLong(SearchSuggestionQuery.DATA_ID);
+ suggestion.photoUri =
+ ContentUris.withAppendedId(Data.CONTENT_URI, dataId).toString();
+ }
+ } else if (Organization.CONTENT_ITEM_TYPE.equals(mimetype)) {
+ if (isSuperPrimary || suggestion.organization == null) {
+ suggestion.organization = c.getString(SearchSuggestionQuery.DATA2);
+ }
+ } else if (Email.CONTENT_ITEM_TYPE.equals(mimetype)) {
+ if (isSuperPrimary || suggestion.email == null) {
+ suggestion.email = c.getString(SearchSuggestionQuery.DATA2);
+ }
+ } else if (Phone.CONTENT_ITEM_TYPE.equals(mimetype)) {
+ if (isSuperPrimary || suggestion.phoneNumber == null) {
+ suggestion.phoneNumber = c.getString(SearchSuggestionQuery.DATA2);
+ }
+ }
+ }
+ } 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());
+ }
+
+ return new ArrayListCursor(SEARCH_SUGGESTIONS_BASED_ON_NAME_COLUMNS, rows);
+ }
+
+ /**
* List of package names with access to {@link RawContacts#IS_RESTRICTED} data.
*/
private static final String[] sAllowedPackages = new String[] {
@@ -2705,6 +3026,10 @@
case AGGREGATION_EXCEPTIONS: return AggregationExceptions.CONTENT_TYPE;
case AGGREGATION_EXCEPTION_ID: return AggregationExceptions.CONTENT_ITEM_TYPE;
case AGGREGATION_SUGGESTIONS: return Contacts.CONTENT_TYPE;
+ case SEARCH_SUGGESTIONS:
+ return SearchManager.SUGGEST_MIME_TYPE;
+ case SEARCH_SHORTCUT:
+ return SearchManager.SHORTCUT_MIME_TYPE;
}
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
@@ -2825,17 +3150,26 @@
filter.append(" WHERE ");
filter.append(RawContacts._ID);
filter.append(" IN ");
- filter.append(getRawContactsByFilterAsNestedQuery(filterParam));
+ appendRawContactsByFilterAsNestedQuery(filter, filterParam, null);
filter.append(")");
return filter.toString();
}
public String getRawContactsByFilterAsNestedQuery(String filterParam) {
- // NOTE: Query parameters won't work here since the SQL compiler
- // needs to parse the actual string to know that it can use the
- // index to do a prefix scan.
- return "(SELECT raw_contact_id FROM name_lookup WHERE normalized_name GLOB '"
- + NameNormalizer.normalize(filterParam) + "*')";
+ StringBuilder sb = new StringBuilder();
+ appendRawContactsByFilterAsNestedQuery(sb, filterParam, null);
+ return sb.toString();
+ }
+
+ private void appendRawContactsByFilterAsNestedQuery(StringBuilder sb, String filterParam,
+ String limit) {
+ sb.append("(SELECT DISTINCT raw_contact_id FROM name_lookup WHERE normalized_name GLOB '");
+ sb.append(NameNormalizer.normalize(filterParam));
+ sb.append("*'");
+ if (limit != null) {
+ sb.append(" LIMIT ").append(limit);
+ }
+ sb.append(")");
}
private String[] appendGroupArg(String[] selectionArgs, String arg) {
diff --git a/src/com/android/providers/contacts/LegacyApiSupport.java b/src/com/android/providers/contacts/LegacyApiSupport.java
index c9d7113..d04bf63 100644
--- a/src/com/android/providers/contacts/LegacyApiSupport.java
+++ b/src/com/android/providers/contacts/LegacyApiSupport.java
@@ -15,15 +15,15 @@
*/
package com.android.providers.contacts;
-import com.android.providers.contacts.OpenHelper.RawContactsColumns;
import com.android.providers.contacts.OpenHelper.DataColumns;
import com.android.providers.contacts.OpenHelper.ExtensionsColumns;
import com.android.providers.contacts.OpenHelper.GroupsColumns;
-import com.android.providers.contacts.OpenHelper.GroupMembershipColumns;
import com.android.providers.contacts.OpenHelper.MimetypesColumns;
import com.android.providers.contacts.OpenHelper.PhoneColumns;
+import com.android.providers.contacts.OpenHelper.RawContactsColumns;
import com.android.providers.contacts.OpenHelper.Tables;
+import android.app.SearchManager;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
@@ -34,15 +34,13 @@
import android.database.sqlite.SQLiteQueryBuilder;
import android.database.sqlite.SQLiteStatement;
import android.net.Uri;
-import android.provider.BaseColumns;
import android.provider.ContactsContract;
import android.provider.Contacts.ContactMethods;
import android.provider.Contacts.People;
-import android.provider.ContactsContract.CommonDataKinds;
-import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.Groups;
import android.provider.ContactsContract.Presence;
+import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
import android.provider.ContactsContract.CommonDataKinds.Im;
@@ -52,7 +50,7 @@
import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
-import android.text.TextUtils;
+import android.util.Log;
import java.util.HashMap;
@@ -91,6 +89,8 @@
private static final int PEOPLE_FILTER = 29;
private static final int DELETED_PEOPLE = 30;
private static final int DELETED_GROUPS = 31;
+ private static final int SEARCH_SUGGESTIONS = 32;
+
private static final String PEOPLE_JOINS =
@@ -277,10 +277,10 @@
matcher.addURI(authority, "organizations", ORGANIZATIONS);
matcher.addURI(authority, "organizations/#", ORGANIZATIONS_ID);
// matcher.addURI(authority, "voice_dialer_timestamp", VOICE_DIALER_TIMESTAMP);
-// matcher.addURI(authority, SearchManager.SUGGEST_URI_PATH_QUERY,
-// SEARCH_SUGGESTIONS);
-// matcher.addURI(authority, SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
-// SEARCH_SUGGESTIONS);
+ matcher.addURI(authority, SearchManager.SUGGEST_URI_PATH_QUERY,
+ SEARCH_SUGGESTIONS);
+ matcher.addURI(authority, SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
+ SEARCH_SUGGESTIONS);
// matcher.addURI(authority, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/#",
// SEARCH_SHORTCUT);
// matcher.addURI(authority, "settings", SETTINGS);
@@ -292,11 +292,6 @@
// LIVE_FOLDERS_PEOPLE_WITH_PHONES);
// matcher.addURI(authority, "live_folders/favorites",
// LIVE_FOLDERS_PEOPLE_FAVORITES);
-//
-// // Call log URI matching table
-// matcher.addURI(CALL_LOG_AUTHORITY, "calls", CALLS);
-// matcher.addURI(CALL_LOG_AUTHORITY, "calls/filter/*", CALLS_FILTER);
-// matcher.addURI(CALL_LOG_AUTHORITY, "calls/#", CALLS_ID);
HashMap<String, String> peopleProjectionMap = new HashMap<String, String>();
@@ -1094,11 +1089,10 @@
}
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
- String sortOrder) {
+ String sortOrder, String limit) {
final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
String groupBy = null;
- String limit = null;
final int match = sUriMatcher.match(uri);
switch (match) {
@@ -1305,6 +1299,11 @@
qb.appendWhere(uri.getPathSegments().get(1));
break;
+ case SEARCH_SUGGESTIONS:
+
+ // No legacy compatibility for search suggestions
+ return mContactsProvider.handleSearchSuggestionsQuery(uri, limit);
+
case DELETED_PEOPLE:
case DELETED_GROUPS:
throw new UnsupportedOperationException();
diff --git a/src/com/android/providers/contacts/OpenHelper.java b/src/com/android/providers/contacts/OpenHelper.java
index 74e2761..8521770 100644
--- a/src/com/android/providers/contacts/OpenHelper.java
+++ b/src/com/android/providers/contacts/OpenHelper.java
@@ -91,8 +91,8 @@
+ "LEFT OUTER JOIN mimetypes ON (data.mimetype_id = mimetypes._id)";
public static final String DATA_JOIN_MIMETYPE_RAW_CONTACTS = "data "
- + "LEFT OUTER JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
- + "LEFT OUTER JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id)";
+ + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
+ + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id)";
public static final String DATA_JOIN_RAW_CONTACTS_GROUPS = "data "
+ "LEFT OUTER JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id)"
@@ -118,6 +118,11 @@
+ "LEFT OUTER JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
+ "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
+ public static final String DATA_INNER_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS = "data "
+ + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
+ + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
+ + "JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
+
public static final String DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS_GROUPS =
"data "
+ "LEFT OUTER JOIN packages ON (data.package_id = packages._id) "
@@ -1149,4 +1154,27 @@
public SyncStateContentProviderHelper getSyncState() {
return mSyncState;
}
+
+ /**
+ * Delete the aggregate contact if it has no constituent raw contacts other
+ * than the supplied one.
+ */
+ public void removeContactIfSingleton(long rawContactId) {
+ SQLiteDatabase db = getWritableDatabase();
+
+ // Obtain contact ID from the supplied raw contact ID
+ String contactIdFromRawContactId = "(SELECT " + RawContacts.CONTACT_ID + " FROM "
+ + Tables.RAW_CONTACTS + " WHERE " + RawContacts._ID + "=" + rawContactId + ")";
+
+ // Find other raw contacts in the same aggregate contact
+ String otherRawContacts = "(SELECT contacts1." + RawContacts._ID + " FROM "
+ + Tables.RAW_CONTACTS + " contacts1 JOIN " + Tables.RAW_CONTACTS + " contacts2 ON ("
+ + "contacts1." + RawContacts.CONTACT_ID + "=contacts2." + RawContacts.CONTACT_ID
+ + ") WHERE contacts1." + RawContacts._ID + "!=" + rawContactId + ""
+ + " AND contacts2." + RawContacts._ID + "=" + rawContactId + ")";
+
+ db.execSQL("DELETE FROM " + Tables.CONTACTS
+ + " WHERE " + Contacts._ID + "=" + contactIdFromRawContactId
+ + " AND NOT EXISTS " + otherRawContacts + ";");
+ }
}
diff --git a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
index 77f9c00..e4186d7 100644
--- a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
@@ -112,6 +112,12 @@
return createRawContact(null);
}
+ protected long createRawContactWithName() {
+ long rawContactId = createRawContact(null);
+ insertStructuredName(rawContactId, "John", "Doe");
+ return rawContactId;
+ }
+
protected long createRawContact(Account account, String... extras) {
ContentValues values = new ContentValues();
for (int i = 0; i < extras.length; ) {
@@ -516,12 +522,17 @@
protected void assertSelection(Uri uri, ContentValues values, String idColumn, long id) {
StringBuilder sb = new StringBuilder();
ArrayList<String> selectionArgs = new ArrayList<String>(values.size());
- sb.append(idColumn).append("=").append(id);
+ if (idColumn != null) {
+ sb.append(idColumn).append("=").append(id);
+ }
Set<Map.Entry<String, Object>> entries = values.valueSet();
for (Map.Entry<String, Object> entry : entries) {
String column = entry.getKey();
Object value = entry.getValue();
- sb.append(" AND ").append(column);
+ if (sb.length() != 0) {
+ sb.append(" AND ");
+ }
+ sb.append(column);
if (value == null) {
sb.append(" IS NULL");
} else {
diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
index cf70a27..2256357 100644
--- a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
@@ -17,26 +17,36 @@
import com.android.internal.util.ArrayUtils;
+import android.app.SearchManager;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Entity;
import android.content.EntityIterator;
+import android.content.res.Resources;
import android.database.Cursor;
+import android.database.DatabaseUtils;
import android.net.Uri;
import android.os.RemoteException;
-import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract;
+import android.provider.Contacts.Intents;
import android.provider.ContactsContract.AggregationExceptions;
+import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.Presence;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
-import android.provider.ContactsContract;
import android.test.suitebuilder.annotation.LargeTest;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
/**
* Unit tests for {@link ContactsProvider2}.
*
@@ -69,7 +79,7 @@
}
public void testSendToVoicemailDefault() {
- long rawContactId = createRawContact();
+ long rawContactId = createRawContactWithName();
long contactId = queryContactId(rawContactId);
Cursor c = queryContact(contactId);
@@ -80,7 +90,7 @@
}
public void testSetSendToVoicemailAndRingtone() {
- long rawContactId = createRawContact();
+ long rawContactId = createRawContactWithName();
long contactId = queryContactId(rawContactId);
updateSendToVoicemailAndRingtone(contactId, true, "foo");
@@ -88,11 +98,11 @@
}
public void testSendToVoicemailAndRingtoneAfterAggregation() {
- long rawContactId1 = createRawContact();
+ long rawContactId1 = createRawContactWithName();
long contactId1 = queryContactId(rawContactId1);
updateSendToVoicemailAndRingtone(contactId1, true, "foo");
- long rawContactId2 = createRawContact();
+ long rawContactId2 = createRawContactWithName();
long contactId2 = queryContactId(rawContactId2);
updateSendToVoicemailAndRingtone(contactId2, true, "bar");
@@ -104,11 +114,11 @@
}
public void testDoNotSendToVoicemailAfterAggregation() {
- long rawContactId1 = createRawContact();
+ long rawContactId1 = createRawContactWithName();
long contactId1 = queryContactId(rawContactId1);
updateSendToVoicemailAndRingtone(contactId1, true, null);
- long rawContactId2 = createRawContact();
+ long rawContactId2 = createRawContactWithName();
long contactId2 = queryContactId(rawContactId2);
updateSendToVoicemailAndRingtone(contactId2, false, null);
@@ -120,11 +130,11 @@
}
public void testSetSendToVoicemailAndRingtonePreservedAfterJoinAndSplit() {
- long rawContactId1 = createRawContact();
+ long rawContactId1 = createRawContactWithName();
long contactId1 = queryContactId(rawContactId1);
updateSendToVoicemailAndRingtone(contactId1, true, "foo");
- long rawContactId2 = createRawContact();
+ long rawContactId2 = createRawContactWithName();
long contactId2 = queryContactId(rawContactId2);
updateSendToVoicemailAndRingtone(contactId2, false, "bar");
@@ -467,5 +477,214 @@
version++;
assertEquals(version, getVersion(uri));
}
+
+ // TODO fix and enable test
+ public void __testSearchSuggestionsNotInMyContacts() throws Exception {
+
+ long rawContactId = createRawContact();
+ insertStructuredName(rawContactId, "Deer", "Dough");
+
+ Uri searchUri = new Uri.Builder().scheme("content").authority(ContactsContract.AUTHORITY)
+ .appendPath(SearchManager.SUGGEST_URI_PATH_QUERY).appendPath("D").build();
+
+ // If the contact is not in the "my contacts" group, nothing should be found
+ Cursor c = mResolver.query(searchUri, null, null, null, null);
+ assertEquals(0, c.getCount());
+ c.close();
+ }
+
+ public void testSearchSuggestionsByName() throws Exception {
+ assertSearchSuggestion(
+ true, // name
+ true, // photo
+ false, // organization
+ false, // phone
+ false, // email
+ "D", // query
+ true, // expect icon URI
+ null, "Deer Dough", null);
+
+ assertSearchSuggestion(
+ true, // name
+ true, // photo
+ true, // organization
+ false, // phone
+ false, // email
+ "D", // query
+ true, // expect icon URI
+ null, "Deer Dough", "Google");
+
+ assertSearchSuggestion(
+ true, // name
+ true, // photo
+ false, // organization
+ true, // phone
+ false, // email
+ "D", // query
+ true, // expect icon URI
+ null, "Deer Dough", "1-800-4664-411");
+
+ assertSearchSuggestion(
+ true, // name
+ true, // photo
+ false, // organization
+ false, // phone
+ true, // email
+ "D", // query
+ true, // expect icon URI
+ String.valueOf(Presence.getPresenceIconResourceId(Presence.OFFLINE)),
+ "Deer Dough", "foo@acme.com");
+
+ assertSearchSuggestion(
+ true, // name
+ false, // photo
+ true, // organization
+ false, // phone
+ false, // email
+ "D", // query
+ false, // expect icon URI
+ null, "Deer Dough", "Google");
+ }
+
+ private void assertSearchSuggestion(boolean name, boolean photo, boolean organization,
+ boolean phone, boolean email, String query, boolean expectIcon1Uri, String expectedIcon2,
+ String expectedText1, String expectedText2) throws IOException {
+ ContentValues values = new ContentValues();
+
+ long rawContactId = createRawContact();
+
+ if (name) {
+ insertStructuredName(rawContactId, "Deer", "Dough");
+ }
+
+
+ final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+ if (photo) {
+ values.clear();
+ byte[] photoData = loadTestPhoto();
+ values.put(Data.RAW_CONTACT_ID, rawContactId);
+ values.put(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
+ values.put(Photo.PHOTO, photoData);
+ mResolver.insert(Data.CONTENT_URI, values);
+ }
+
+ if (organization) {
+ values.clear();
+ values.put(Data.RAW_CONTACT_ID, rawContactId);
+ values.put(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE);
+ values.put(Organization.TYPE, Organization.TYPE_WORK);
+ values.put(Organization.COMPANY, "Google");
+ mResolver.insert(Data.CONTENT_URI, values);
+ }
+
+ if (email) {
+ values.clear();
+ values.put(Data.RAW_CONTACT_ID, rawContactId);
+ values.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+ values.put(Email.TYPE, Email.TYPE_WORK);
+ values.put(Email.DATA, "foo@acme.com");
+ mResolver.insert(Data.CONTENT_URI, values);
+
+ String protocol = Im.encodePredefinedImProtocol(Im.PROTOCOL_GOOGLE_TALK);
+
+ values.clear();
+ values.put(Presence.IM_PROTOCOL, protocol);
+ values.put(Presence.IM_HANDLE, "foo@acme.com");
+ values.put(Presence.IM_ACCOUNT, "foo");
+ values.put(Presence.PRESENCE_STATUS, Presence.OFFLINE);
+ values.put(Presence.PRESENCE_CUSTOM_STATUS, "Coding for Android");
+ mResolver.insert(Presence.CONTENT_URI, values);
+ }
+
+ if (phone) {
+ values.clear();
+ values.put(Data.RAW_CONTACT_ID, rawContactId);
+ values.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+ values.put(Data.IS_PRIMARY, 1);
+ values.put(Phone.TYPE, Phone.TYPE_HOME);
+ values.put(Phone.NUMBER, "1-800-4664-411");
+ mResolver.insert(Data.CONTENT_URI, values);
+ }
+
+ long contactId = queryContactId(rawContactId);
+ Uri searchUri = new Uri.Builder().scheme("content").authority(ContactsContract.AUTHORITY)
+ .appendPath(SearchManager.SUGGEST_URI_PATH_QUERY).appendPath(query).build();
+
+ Cursor c = mResolver.query(searchUri, null, null, null, null);
+ DatabaseUtils.dumpCursor(c);
+ assertEquals(1, c.getCount());
+ c.moveToFirst();
+ values.clear();
+
+ // SearchManager does not declare a constant for _id
+ values.put("_id", contactId);
+ values.put(SearchManager.SUGGEST_COLUMN_TEXT_1, expectedText1);
+ values.put(SearchManager.SUGGEST_COLUMN_TEXT_2, expectedText2);
+
+ String icon1 = c.getString(c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1));
+ if (expectIcon1Uri) {
+ assertTrue(icon1.startsWith("content:"));
+ } else {
+ assertEquals(String.valueOf(com.android.internal.R.drawable.ic_contact_picture), icon1);
+ }
+
+ values.put(SearchManager.SUGGEST_COLUMN_ICON_2, expectedIcon2);
+ values.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID, contactId);
+ values.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, contactId);
+ assertCursorValues(c, values);
+ c.close();
+
+ // Cleanup
+ mResolver.delete(rawContactUri, null, null);
+ }
+
+ public void testSearchSuggestionsByPhoneNumber() throws Exception {
+ ContentValues values = new ContentValues();
+
+ Uri searchUri = new Uri.Builder().scheme("content").authority(ContactsContract.AUTHORITY)
+ .appendPath(SearchManager.SUGGEST_URI_PATH_QUERY).appendPath("12345").build();
+
+ Cursor c = mResolver.query(searchUri, null, null, null, null);
+ DatabaseUtils.dumpCursor(c);
+ assertEquals(2, c.getCount());
+ c.moveToFirst();
+
+ values.put(SearchManager.SUGGEST_COLUMN_TEXT_1, "Dial number");
+ values.put(SearchManager.SUGGEST_COLUMN_TEXT_2, "using 12345");
+ values.put(SearchManager.SUGGEST_COLUMN_ICON_1,
+ String.valueOf(com.android.internal.R.drawable.call_contact));
+ values.put(SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
+ Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED);
+ values.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA, "tel:12345");
+ values.putNull(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID);
+ assertCursorValues(c, values);
+
+ c.moveToNext();
+ values.clear();
+ values.put(SearchManager.SUGGEST_COLUMN_TEXT_1, "Create contact");
+ values.put(SearchManager.SUGGEST_COLUMN_TEXT_2, "using 12345");
+ values.put(SearchManager.SUGGEST_COLUMN_ICON_1,
+ String.valueOf(com.android.internal.R.drawable.create_contact));
+ values.put(SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
+ Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED);
+ values.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA, "tel:12345");
+ values.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
+ SearchManager.SUGGEST_NEVER_MAKE_SHORTCUT);
+ assertCursorValues(c, values);
+ c.close();
+ }
+
+ private byte[] loadTestPhoto() throws IOException {
+ final Resources resources = getContext().getResources();
+ InputStream is =
+ resources.openRawResource(com.android.internal.R.drawable.ic_contact_picture);
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ byte[] buffer = new byte[1000];
+ int count;
+ while((count = is.read(buffer)) != -1) {
+ os.write(buffer, 0, count);
+ }
+ return os.toByteArray();
+ }
}
diff --git a/tests/src/com/android/providers/contacts/LegacyContactsProviderTest.java b/tests/src/com/android/providers/contacts/LegacyContactsProviderTest.java
index b50ed09..502010d 100644
--- a/tests/src/com/android/providers/contacts/LegacyContactsProviderTest.java
+++ b/tests/src/com/android/providers/contacts/LegacyContactsProviderTest.java
@@ -16,6 +16,7 @@
package com.android.providers.contacts;
+import android.app.SearchManager;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
@@ -27,6 +28,7 @@
import android.provider.Contacts.Extensions;
import android.provider.Contacts.GroupMembership;
import android.provider.Contacts.Groups;
+import android.provider.Contacts.Intents;
import android.provider.Contacts.Organizations;
import android.provider.Contacts.People;
import android.provider.Contacts.Phones;
@@ -516,7 +518,8 @@
Uri presenceUri = mResolver.insert(Presence.CONTENT_URI, values);
- values.put(Presence.PERSON_ID, personId);
+ // FIXME person_id was not available in legacy ContactsProvider
+ // values.put(Presence.PERSON_ID, personId);
assertStoredValues(presenceUri, values);
assertSelection(Presence.CONTENT_URI, values,
Presence._ID, ContentUris.parseId(presenceUri));
@@ -541,7 +544,8 @@
values.clear();
values.put(Photos.DATA, photo);
values.put(Photos.LOCAL_VERSION, "10");
- values.put(Photos.DOWNLOAD_REQUIRED, 1);
+ // FIXME this column was unavailable for update in legacy ContactsProvider
+ // values.put(Photos.DOWNLOAD_REQUIRED, 1);
values.put(Photos.EXISTS_ON_SERVER, 1);
values.put(Photos.SYNC_ERROR, "404 does not exist");
@@ -550,6 +554,217 @@
assertStoredValues(photoUri, values);
}
+ /**
+ * Capturing the search suggestion requirements in test cases as a reference.
+ */
+ public void testSearchSuggestionsNotInMyContacts() throws Exception {
+
+ // We don't provide compatibility for search suggestions
+ if (!USE_LEGACY_PROVIDER) {
+ return;
+ }
+
+ ContentValues values = new ContentValues();
+ putContactValues(values);
+ mResolver.insert(People.CONTENT_URI, values);
+
+ Uri searchUri = new Uri.Builder().scheme("content").authority(Contacts.AUTHORITY)
+ .appendPath(SearchManager.SUGGEST_URI_PATH_QUERY).appendPath("D").build();
+
+ // If the contact is not in the "my contacts" group, nothing should be found
+ Cursor c = mResolver.query(searchUri, null, null, null, null);
+ assertEquals(0, c.getCount());
+ c.close();
+ }
+
+ /**
+ * Capturing the search suggestion requirements in test cases as a reference.
+ */
+ public void testSearchSuggestionsByName() throws Exception {
+
+ // We don't provide compatibility for search suggestions
+ if (!USE_LEGACY_PROVIDER) {
+ return;
+ }
+
+ assertSearchSuggestion(
+ true, // name
+ true, // photo
+ true, // organization
+ false, // phone
+ false, // email
+ "D", // query
+ true, // expect icon URI
+ null, "Deer Dough", "Google");
+
+ assertSearchSuggestion(
+ true, // name
+ true, // photo
+ false, // organization
+ true, // phone
+ false, // email
+ "D", // query
+ true, // expect icon URI
+ null, "Deer Dough", "1-800-4664-411");
+
+ assertSearchSuggestion(
+ true, // name
+ true, // photo
+ false, // organization
+ false, // phone
+ true, // email
+ "D", // query
+ true, // expect icon URI
+ String.valueOf(Presence.getPresenceIconResourceId(Presence.OFFLINE)),
+ "Deer Dough", "foo@acme.com");
+
+ assertSearchSuggestion(
+ true, // name
+ false, // photo
+ true, // organization
+ false, // phone
+ false, // email
+ "D", // query
+ false, // expect icon URI
+ null, "Deer Dough", "Google");
+ }
+
+ private void assertSearchSuggestion(boolean name, boolean photo, boolean organization,
+ boolean phone, boolean email, String query, boolean expectIcon1Uri, String expectedIcon2,
+ String expectedText1, String expectedText2) throws IOException {
+ ContentValues values = new ContentValues();
+
+ if (name) {
+ values.put(People.NAME, "Deer Dough");
+ }
+
+ final Uri personUri = mResolver.insert(People.CONTENT_URI, values);
+ long personId = ContentUris.parseId(personUri);
+
+ People.addToMyContactsGroup(mResolver, personId);
+
+ if (photo) {
+ values.clear();
+ byte[] photoData = loadTestPhoto();
+ values.put(Photos.DATA, photoData);
+ values.put(Photos.LOCAL_VERSION, "1");
+ values.put(Photos.EXISTS_ON_SERVER, 0);
+ Uri photoUri = Uri.withAppendedPath(personUri, Photos.CONTENT_DIRECTORY);
+ mResolver.update(photoUri, values, null, null);
+ }
+
+ if (organization) {
+ values.clear();
+ values.put(Organizations.ISPRIMARY, 1);
+ values.put(Organizations.COMPANY, "Google");
+ values.put(Organizations.TYPE, Organizations.TYPE_WORK);
+ values.put(Organizations.PERSON_ID, personId);
+ mResolver.insert(Organizations.CONTENT_URI, values);
+ }
+
+ if (email) {
+ values.clear();
+ values.put(ContactMethods.PERSON_ID, personId);
+ values.put(ContactMethods.KIND, Contacts.KIND_EMAIL);
+ values.put(ContactMethods.TYPE, ContactMethods.TYPE_HOME);
+ values.put(ContactMethods.DATA, "foo@acme.com");
+ values.put(ContactMethods.ISPRIMARY, 1);
+ mResolver.insert(ContactMethods.CONTENT_URI, values);
+
+
+ String protocol = ContactMethods
+ .encodePredefinedImProtocol(ContactMethods.PROTOCOL_GOOGLE_TALK);
+ values.clear();
+ values.put(Presence.IM_PROTOCOL, protocol);
+ values.put(Presence.IM_HANDLE, "foo@acme.com");
+ values.put(Presence.IM_ACCOUNT, "foo");
+ values.put(Presence.PRESENCE_STATUS, Presence.OFFLINE);
+ values.put(Presence.PRESENCE_CUSTOM_STATUS, "Coding for Android");
+ mResolver.insert(Presence.CONTENT_URI, values);
+ }
+
+ if (phone) {
+ values.clear();
+ values.put(Phones.PERSON_ID, personId);
+ values.put(Phones.TYPE, Phones.TYPE_HOME);
+ values.put(Phones.NUMBER, "1-800-4664-411");
+ values.put(Phones.ISPRIMARY, 1);
+ mResolver.insert(Phones.CONTENT_URI, values);
+ }
+
+ Uri searchUri = new Uri.Builder().scheme("content").authority(Contacts.AUTHORITY)
+ .appendPath(SearchManager.SUGGEST_URI_PATH_QUERY).appendPath(query).build();
+
+ Cursor c = mResolver.query(searchUri, null, null, null, null);
+ assertEquals(1, c.getCount());
+ c.moveToFirst();
+ values.clear();
+
+ String icon1 = c.getString(c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1));
+ if (expectIcon1Uri) {
+ assertTrue(icon1.startsWith("content:"));
+ } else {
+ assertEquals(String.valueOf(com.android.internal.R.drawable.ic_contact_picture), icon1);
+ }
+
+ // SearchManager does not declare a constant for _id
+ values.put("_id", personId);
+ values.put(SearchManager.SUGGEST_COLUMN_ICON_2, expectedIcon2);
+ values.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID, personId);
+ values.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, personId);
+ values.put(SearchManager.SUGGEST_COLUMN_TEXT_1, expectedText1);
+ values.put(SearchManager.SUGGEST_COLUMN_TEXT_2, expectedText2);
+ assertCursorValues(c, values);
+ c.close();
+
+ // Cleanup
+ mResolver.delete(personUri, null, null);
+ }
+
+ /**
+ * Capturing the search suggestion requirements in test cases as a reference.
+ */
+ public void testSearchSuggestionsByPhoneNumber() throws Exception {
+
+ // We don't provide compatibility for search suggestions
+ if (!USE_LEGACY_PROVIDER) {
+ return;
+ }
+
+ ContentValues values = new ContentValues();
+
+ Uri searchUri = new Uri.Builder().scheme("content").authority(Contacts.AUTHORITY)
+ .appendPath(SearchManager.SUGGEST_URI_PATH_QUERY).appendPath("12345").build();
+
+ Cursor c = mResolver.query(searchUri, null, null, null, null);
+ assertEquals(2, c.getCount());
+ c.moveToFirst();
+
+ values.put(SearchManager.SUGGEST_COLUMN_TEXT_1, "Execute");
+ values.put(SearchManager.SUGGEST_COLUMN_TEXT_2, "");
+ values.put(SearchManager.SUGGEST_COLUMN_ICON_1,
+ String.valueOf(com.android.internal.R.drawable.call_contact));
+ values.put(SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
+ Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED);
+ values.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA, "tel:12345");
+ values.putNull(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID);
+ assertCursorValues(c, values);
+
+ c.moveToNext();
+ values.clear();
+ values.put(SearchManager.SUGGEST_COLUMN_TEXT_1, "Dial number");
+ values.put(SearchManager.SUGGEST_COLUMN_TEXT_2, "using 12345");
+ values.put(SearchManager.SUGGEST_COLUMN_ICON_1,
+ String.valueOf(com.android.internal.R.drawable.create_contact));
+ values.put(SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
+ Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED);
+ values.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA, "tel:12345");
+ values.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
+ SearchManager.SUGGEST_NEVER_MAKE_SHORTCUT);
+ assertCursorValues(c, values);
+ c.close();
+ }
+
private void putContactValues(ContentValues values) {
// Populating only unhidden columns
values.put(People.NAME, "Deer Dough");
@@ -634,4 +849,14 @@
}
}
+ @Override
+ protected void assertSelection(Uri uri, ContentValues values, String idColumn, long id) {
+ if (USE_LEGACY_PROVIDER) {
+ // A bug in the legacy ContactsProvider prevents us from using the
+ // _id column in selection.
+ super.assertSelection(uri, values, null, 0);
+ } else {
+ super.assertSelection(uri, values, idColumn, id);
+ }
+ }
}