Revert "Remove global search support."

The global search API is not being deprecated in this release, so we
want to keep the support for it.
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index fb8e961..a239275 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -34,6 +34,15 @@
             android:exported="true"
             android:readPermission="android.permission.READ_CONTACTS"
             android:writePermission="android.permission.WRITE_CONTACTS">
+            <path-permission
+                    android:pathPrefix="/search_suggest_query"
+                    android:readPermission="android.permission.GLOBAL_SEARCH" />
+            <path-permission
+                    android:pathPrefix="/search_suggest_shortcut"
+                    android:readPermission="android.permission.GLOBAL_SEARCH" />
+            <path-permission
+                    android:pathPattern="/contacts/.*/photo"
+                    android:readPermission="android.permission.GLOBAL_SEARCH" />
             <grant-uri-permission android:pathPattern=".*" />
         </provider>
 
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index dd4ecff..85c9746 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -339,6 +339,9 @@
     private static final int PROFILE_SYNCSTATE = 11002;
     private static final int PROFILE_SYNCSTATE_ID = 11003;
 
+    private static final int SEARCH_SUGGESTIONS = 12001;
+    private static final int SEARCH_SHORTCUT = 12002;
+
     private static final int RAW_CONTACT_ENTITIES = 15001;
 
     private static final int PROVIDER_STATUS = 16001;
@@ -1222,6 +1225,13 @@
         matcher.addURI(ContactsContract.AUTHORITY, "status_updates", STATUS_UPDATES);
         matcher.addURI(ContactsContract.AUTHORITY, "status_updates/#", STATUS_UPDATES_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);
+
         matcher.addURI(ContactsContract.AUTHORITY, "provider_status", PROVIDER_STATUS);
 
         matcher.addURI(ContactsContract.AUTHORITY, "directories", DIRECTORIES);
@@ -1367,6 +1377,7 @@
     private final SecureRandom mRandom = new SecureRandom();
 
     private LegacyApiSupport mLegacyApiSupport;
+    private GlobalSearchSupport mGlobalSearchSupport;
     private CommonNicknameCache mCommonNicknameCache;
     private SearchIndexManager mSearchIndexManager;
 
@@ -1442,6 +1453,7 @@
         setDbHelperToSerializeOn(mContactsHelper, CONTACTS_DB_TAG, this);
 
         mContactDirectoryManager = new ContactDirectoryManager(this);
+        mGlobalSearchSupport = new GlobalSearchSupport(this);
 
         // The provider is closed for business until fully initialized
         mReadAccessLatch = new CountDownLatch(1);
@@ -1486,7 +1498,8 @@
      */
     private void initForDefaultLocale() {
         Context context = getContext();
-        mLegacyApiSupport = new LegacyApiSupport(context, mContactsHelper, this);
+        mLegacyApiSupport = new LegacyApiSupport(context, mContactsHelper, this,
+                mGlobalSearchSupport);
         mCurrentLocale = getLocale();
         mNameSplitter = mContactsHelper.createNameSplitter(mCurrentLocale);
         mNameLookupBuilder = new StructuredNameLookupBuilder(mNameSplitter);
@@ -6291,6 +6304,19 @@
                 break;
             }
 
+            case SEARCH_SUGGESTIONS: {
+                return mGlobalSearchSupport.handleSearchSuggestionsQuery(
+                        db, uri, projection, limit, cancellationSignal);
+            }
+
+            case SEARCH_SHORTCUT: {
+                String lookupKey = uri.getLastPathSegment();
+                String filter = getQueryParameter(
+                        uri, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
+                return mGlobalSearchSupport.handleSearchShortcutRefresh(
+                        db, projection, lookupKey, filter, cancellationSignal);
+            }
+
             case RAW_CONTACT_ENTITIES:
             case PROFILE_RAW_CONTACT_ENTITIES: {
                 setTablesAndProjectionMapForRawEntities(qb, uri);
@@ -8068,6 +8094,10 @@
                 return Settings.CONTENT_TYPE;
             case AGGREGATION_SUGGESTIONS:
                 return Contacts.CONTENT_TYPE;
+            case SEARCH_SUGGESTIONS:
+                return SearchManager.SUGGEST_MIME_TYPE;
+            case SEARCH_SHORTCUT:
+                return SearchManager.SHORTCUT_MIME_TYPE;
             case DIRECTORIES:
                 return Directory.CONTENT_TYPE;
             case DIRECTORIES_ID:
diff --git a/src/com/android/providers/contacts/GlobalSearchSupport.java b/src/com/android/providers/contacts/GlobalSearchSupport.java
new file mode 100644
index 0000000..0febf56
--- /dev/null
+++ b/src/com/android/providers/contacts/GlobalSearchSupport.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.providers.contacts;
+
+import android.app.SearchManager;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.os.CancellationSignal;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.SearchSnippetColumns;
+import android.provider.ContactsContract.StatusUpdates;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+
+import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
+import com.android.providers.contacts.ContactsDatabaseHelper.Views;
+
+import java.util.ArrayList;
+
+/**
+ * Support for global search integration for Contacts.
+ */
+public class GlobalSearchSupport {
+
+    private static final String[] SEARCH_SUGGESTIONS_COLUMNS = {
+            "_id",
+            SearchManager.SUGGEST_COLUMN_TEXT_1,
+            SearchManager.SUGGEST_COLUMN_TEXT_2,
+            SearchManager.SUGGEST_COLUMN_ICON_1,
+            SearchManager.SUGGEST_COLUMN_ICON_2,
+            SearchManager.SUGGEST_COLUMN_INTENT_DATA,
+            SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
+            SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
+            SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA,
+            SearchManager.SUGGEST_COLUMN_LAST_ACCESS_HINT,
+    };
+
+    private static final char SNIPPET_START_MATCH = '\u0001';
+    private static final char SNIPPET_END_MATCH = '\u0001';
+    private static final String SNIPPET_ELLIPSIS = "\u2026";
+    private static final int SNIPPET_MAX_TOKENS = 5;
+
+    private static final String PRESENCE_SQL =
+        "(SELECT " + StatusUpdates.PRESENCE +
+        " FROM " + Tables.AGGREGATED_PRESENCE +
+        " WHERE " + AggregatedPresenceColumns.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + ")";
+
+    private static class SearchSuggestion {
+        long contactId;
+        String photoUri;
+        String lookupKey;
+        int presence = -1;
+        String text1;
+        String text2;
+        String icon1;
+        String icon2;
+        String intentData;
+        String intentAction;
+        String filter;
+        String lastAccessTime;
+
+        @SuppressWarnings({"unchecked"})
+        public ArrayList<?> asList(String[] projection) {
+            if (icon1 == null) {
+                if (photoUri != null) {
+                    icon1 = photoUri.toString();
+                } else {
+                    icon1 = String.valueOf(com.android.internal.R.drawable.ic_contact_picture);
+                }
+            }
+
+            if (presence != -1) {
+                icon2 = String.valueOf(StatusUpdates.getPresenceIconResourceId(presence));
+            }
+
+            ArrayList<Object> list = new ArrayList<Object>();
+            if (projection == null) {
+                list.add(contactId); // _id
+                list.add(text1); // text1
+                list.add(text2); // text2
+                list.add(icon1); // icon1
+                list.add(icon2); // icon2
+                list.add(intentData == null ? buildUri() : intentData); // intent data
+                list.add(intentAction); // intentAction
+                list.add(lookupKey); // shortcut id
+                list.add(filter); // extra data
+                list.add(lastAccessTime); // last access hint
+            } else {
+                for (int i = 0; i < projection.length; i++) {
+                    addColumnValue(list, projection[i]);
+                }
+            }
+            return list;
+        }
+
+        private void addColumnValue(ArrayList<Object> list, String column) {
+            if ("_id".equals(column)) {
+                list.add(contactId);
+            } else if (SearchManager.SUGGEST_COLUMN_TEXT_1.equals(column)) {
+                list.add(text1);
+            } else if (SearchManager.SUGGEST_COLUMN_TEXT_2.equals(column)) {
+                list.add(text2);
+            } else if (SearchManager.SUGGEST_COLUMN_ICON_1.equals(column)) {
+                list.add(icon1);
+            } else if (SearchManager.SUGGEST_COLUMN_ICON_2.equals(column)) {
+                list.add(icon2);
+            } else if (SearchManager.SUGGEST_COLUMN_INTENT_DATA.equals(column)) {
+                list.add(intentData == null ? buildUri() : intentData);
+            } else if (SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID.equals(column)) {
+                list.add(lookupKey);
+            } else if (SearchManager.SUGGEST_COLUMN_SHORTCUT_ID.equals(column)) {
+                list.add(lookupKey);
+            } else if (SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA.equals(column)) {
+                list.add(filter);
+            } else if (SearchManager.SUGGEST_COLUMN_LAST_ACCESS_HINT.equals(column)) {
+                list.add(lastAccessTime);
+            } else {
+                throw new IllegalArgumentException("Invalid column name: " + column);
+            }
+        }
+
+        private String buildUri() {
+            return Contacts.getLookupUri(contactId, lookupKey).toString();
+        }
+
+        public void reset() {
+            contactId = 0;
+            photoUri = null;
+            lookupKey = null;
+            presence = -1;
+            text1 = null;
+            text2 = null;
+            icon1 = null;
+            icon2 = null;
+            intentData = null;
+            intentAction = null;
+            filter = null;
+            lastAccessTime = null;
+        }
+    }
+
+    private final ContactsProvider2 mContactsProvider;
+
+    @SuppressWarnings("all")
+    public GlobalSearchSupport(ContactsProvider2 contactsProvider) {
+        mContactsProvider = contactsProvider;
+
+        TelephonyManager telman = (TelephonyManager)
+                mContactsProvider.getContext().getSystemService(Context.TELEPHONY_SERVICE);
+
+        // To ensure the data column position. This is dead code if properly configured.
+        if (Organization.COMPANY != Data.DATA1 || Phone.NUMBER != Data.DATA1
+                || Email.DATA != Data.DATA1) {
+            throw new AssertionError("Some of ContactsContract.CommonDataKinds class primary"
+                    + " data is not in DATA1 column");
+        }
+    }
+
+    public Cursor handleSearchSuggestionsQuery(SQLiteDatabase db, Uri uri, String[] projection,
+            String limit, CancellationSignal cancellationSignal) {
+        final MatrixCursor cursor = new MatrixCursor(
+                projection == null ? SEARCH_SUGGESTIONS_COLUMNS : projection);
+
+        if (uri.getPathSegments().size() <= 1) {
+            // no search term, return empty
+        } else {
+            String selection = null;
+            String searchClause = uri.getLastPathSegment();
+            addSearchSuggestionsBasedOnFilter(
+                    cursor, db, projection, selection, searchClause, limit, cancellationSignal);
+        }
+
+        return cursor;
+    }
+
+    /**
+     * Returns a search suggestions cursor for the contact bearing the provided lookup key.  If the
+     * lookup key cannot be found in the database, the contact name is decoded from the lookup key
+     * and used to re-identify the contact.  If the contact still cannot be found, an empty cursor
+     * is returned.
+     *
+     * <p>Note that if {@code lookupKey} is not a valid lookup key, an empty cursor is returned
+     * silently.  This would occur with old-style shortcuts that were created using the contact id
+     * instead of the lookup key.
+     */
+    public Cursor handleSearchShortcutRefresh(SQLiteDatabase db, String[] projection,
+            String lookupKey, String filter, CancellationSignal cancellationSignal) {
+        long contactId;
+        try {
+            contactId = mContactsProvider.lookupContactIdByLookupKey(db, lookupKey);
+        } catch (IllegalArgumentException e) {
+            contactId = -1L;
+        }
+        MatrixCursor cursor = new MatrixCursor(
+                projection == null ? SEARCH_SUGGESTIONS_COLUMNS : projection);
+        return addSearchSuggestionsBasedOnFilter(cursor,
+                db, projection, ContactsColumns.CONCRETE_ID + "=" + contactId, filter, null,
+                cancellationSignal);
+    }
+
+    private Cursor addSearchSuggestionsBasedOnFilter(MatrixCursor cursor, SQLiteDatabase db,
+            String[] projection, String selection, String filter, String limit,
+            CancellationSignal cancellationSignal) {
+        StringBuilder sb = new StringBuilder();
+        final boolean haveFilter = !TextUtils.isEmpty(filter);
+        sb.append("SELECT "
+                        + Contacts._ID + ", "
+                        + Contacts.LOOKUP_KEY + ", "
+                        + Contacts.PHOTO_THUMBNAIL_URI + ", "
+                        + Contacts.DISPLAY_NAME + ", "
+                        + PRESENCE_SQL + " AS " + Contacts.CONTACT_PRESENCE + ", "
+                        + Contacts.LAST_TIME_CONTACTED);
+        if (haveFilter) {
+            sb.append(", " + SearchSnippetColumns.SNIPPET);
+        }
+        sb.append(" FROM ");
+        sb.append(Views.CONTACTS);
+        sb.append(" AS contacts");
+        if (haveFilter) {
+            mContactsProvider.appendSearchIndexJoin(sb, filter, true,
+                    String.valueOf(SNIPPET_START_MATCH), String.valueOf(SNIPPET_END_MATCH),
+                    SNIPPET_ELLIPSIS, SNIPPET_MAX_TOKENS, false);
+        }
+        if (selection != null) {
+            sb.append(" WHERE ").append(selection);
+        }
+        if (limit != null) {
+            sb.append(" LIMIT " + limit);
+        }
+        Cursor c = db.rawQuery(sb.toString(), null, cancellationSignal);
+        SearchSuggestion suggestion = new SearchSuggestion();
+        suggestion.filter = filter;
+        try {
+            while (c.moveToNext()) {
+                suggestion.contactId = c.getLong(0);
+                suggestion.lookupKey = c.getString(1);
+                suggestion.photoUri = c.getString(2);
+                suggestion.text1 = c.getString(3);
+                suggestion.presence = c.isNull(4) ? -1 : c.getInt(4);
+                suggestion.lastAccessTime = c.getString(5);
+                if (haveFilter) {
+                    suggestion.text2 = shortenSnippet(c.getString(6));
+                }
+                cursor.addRow(suggestion.asList(projection));
+                suggestion.reset();
+            }
+        } finally {
+            c.close();
+        }
+        return cursor;
+    }
+
+    private String shortenSnippet(final String snippet) {
+        if (snippet == null) {
+            return null;
+        }
+
+        int from = 0;
+        int to = snippet.length();
+        int start = snippet.indexOf(SNIPPET_START_MATCH);
+        if (start == -1) {
+            return null;
+        }
+
+        int firstNl = snippet.lastIndexOf('\n', start);
+        if (firstNl != -1) {
+            from = firstNl + 1;
+        }
+        int end = snippet.lastIndexOf(SNIPPET_END_MATCH);
+        if (end != -1) {
+            int lastNl = snippet.indexOf('\n', end);
+            if (lastNl != -1) {
+                to = lastNl;
+            }
+        }
+
+        StringBuilder sb = new StringBuilder();
+        for (int i = from; i < to; i++) {
+            char c = snippet.charAt(i);
+            if (c != SNIPPET_START_MATCH && c != SNIPPET_END_MATCH) {
+                sb.append(c);
+            }
+        }
+        return sb.toString();
+    }
+}
diff --git a/src/com/android/providers/contacts/LegacyApiSupport.java b/src/com/android/providers/contacts/LegacyApiSupport.java
index da31f15..598a4a0 100644
--- a/src/com/android/providers/contacts/LegacyApiSupport.java
+++ b/src/com/android/providers/contacts/LegacyApiSupport.java
@@ -104,6 +104,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 int SEARCH_SHORTCUT = 33;
     private static final int PHONES_FILTER = 34;
     private static final int LIVE_FOLDERS_PEOPLE = 35;
     private static final int LIVE_FOLDERS_PEOPLE_GROUP_NAME = 36;
@@ -347,6 +349,12 @@
         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_SHORTCUT + "/*",
+                SEARCH_SHORTCUT);
         matcher.addURI(authority, "settings", SETTINGS);
 
         matcher.addURI(authority, "live_folders/people", LIVE_FOLDERS_PEOPLE);
@@ -492,6 +500,7 @@
     private final ContactsDatabaseHelper mDbHelper;
     private final ContactsProvider2 mContactsProvider;
     private final NameSplitter mPhoneticNameSplitter;
+    private final GlobalSearchSupport mGlobalSearchSupport;
 
     private final SQLiteStatement mDataMimetypeQuery;
     private final SQLiteStatement mDataRawContactIdQuery;
@@ -508,10 +517,11 @@
 
 
     public LegacyApiSupport(Context context, ContactsDatabaseHelper contactsDatabaseHelper,
-            ContactsProvider2 contactsProvider) {
+            ContactsProvider2 contactsProvider, GlobalSearchSupport globalSearchSupport) {
         mContext = context;
         mContactsProvider = contactsProvider;
         mDbHelper = contactsDatabaseHelper;
+        mGlobalSearchSupport = globalSearchSupport;
 
         mPhoneticNameSplitter = new NameSplitter("", "", "", context
                 .getString(com.android.internal.R.string.common_name_conjunctions), Locale
@@ -1863,6 +1873,17 @@
                 qb.appendWhere(uri.getPathSegments().get(1));
                 break;
 
+            case SEARCH_SUGGESTIONS:
+                return mGlobalSearchSupport.handleSearchSuggestionsQuery(
+                        db, uri, projection, limit, null);
+
+            case SEARCH_SHORTCUT: {
+                String lookupKey = uri.getLastPathSegment();
+                String filter = ContactsProvider2.getQueryParameter(uri, "filter");
+                return mGlobalSearchSupport.handleSearchShortcutRefresh(
+                        db, projection, lookupKey, filter, null);
+            }
+
             case LIVE_FOLDERS_PEOPLE:
                 return mContactsProvider.query(LIVE_FOLDERS_CONTACTS_URI,
                         projection, selection, selectionArgs, sortOrder);
@@ -2055,6 +2076,10 @@
                 return "vnd.android.cursor.dir/organizations";
             case ORGANIZATIONS_ID:
                 return "vnd.android.cursor.item/organization";
+            case SEARCH_SUGGESTIONS:
+                return SearchManager.SUGGEST_MIME_TYPE;
+            case SEARCH_SHORTCUT:
+                return SearchManager.SHORTCUT_MIME_TYPE;
             default:
                 throw new IllegalArgumentException(mDbHelper.exceptionMessage(uri));
         }