diff options
author | Mathew Inwood <mathewi@google.com> | 2013-09-30 15:49:39 +0100 |
---|---|---|
committer | Mathew Inwood <mathewi@google.com> | 2013-09-30 15:50:50 +0100 |
commit | ec987e500405c067a13b66eeaed1516696790dc4 (patch) | |
tree | 3cdcc7c749d376faf9a1d3bf068a0a39c0ef2dde | |
parent | f2adf6ea72b13670118d22991ac0c2c43ed0d3d4 (diff) | |
download | ContactsProvider-ec987e500405c067a13b66eeaed1516696790dc4.tar.gz |
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.
-rw-r--r-- | AndroidManifest.xml | 9 | ||||
-rw-r--r-- | src/com/android/providers/contacts/ContactsProvider2.java | 32 | ||||
-rw-r--r-- | src/com/android/providers/contacts/GlobalSearchSupport.java | 309 | ||||
-rw-r--r-- | src/com/android/providers/contacts/LegacyApiSupport.java | 27 |
4 files changed, 375 insertions, 2 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index fb8e9619..a239275c 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 dd4ecff2..85c97468 100644 --- a/src/com/android/providers/contacts/ContactsProvider2.java +++ b/src/com/android/providers/contacts/ContactsProvider2.java @@ -339,6 +339,9 @@ public class ContactsProvider2 extends AbstractContactsProvider 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 @@ public class ContactsProvider2 extends AbstractContactsProvider 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 @@ public class ContactsProvider2 extends AbstractContactsProvider private final SecureRandom mRandom = new SecureRandom(); private LegacyApiSupport mLegacyApiSupport; + private GlobalSearchSupport mGlobalSearchSupport; private CommonNicknameCache mCommonNicknameCache; private SearchIndexManager mSearchIndexManager; @@ -1442,6 +1453,7 @@ public class ContactsProvider2 extends AbstractContactsProvider 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 @@ public class ContactsProvider2 extends AbstractContactsProvider */ 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 @@ public class ContactsProvider2 extends AbstractContactsProvider 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 @@ public class ContactsProvider2 extends AbstractContactsProvider 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 00000000..0febf567 --- /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 da31f153..598a4a06 100644 --- a/src/com/android/providers/contacts/LegacyApiSupport.java +++ b/src/com/android/providers/contacts/LegacyApiSupport.java @@ -104,6 +104,8 @@ public class LegacyApiSupport { 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 @@ public class LegacyApiSupport { 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 @@ public class LegacyApiSupport { 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 class LegacyApiSupport { 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 @@ public class LegacyApiSupport { 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 @@ public class LegacyApiSupport { 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)); } |