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);
+        }
+    }
 }