summaryrefslogtreecommitdiffstats
path: root/src/com
diff options
context:
space:
mode:
Diffstat (limited to 'src/com')
-rw-r--r--src/com/android/providers/contacts/ContactsDatabaseHelper.java9
-rw-r--r--src/com/android/providers/contacts/ContactsProvider2.java277
-rw-r--r--src/com/android/providers/contacts/GlobalSearchSupport.java42
-rw-r--r--src/com/android/providers/contacts/SnippetizingCursorWrapper.java280
4 files changed, 481 insertions, 127 deletions
diff --git a/src/com/android/providers/contacts/ContactsDatabaseHelper.java b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
index 3181375..bc76e24 100644
--- a/src/com/android/providers/contacts/ContactsDatabaseHelper.java
+++ b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
@@ -2710,7 +2710,6 @@ import java.util.Locale;
return;
}
- String mCountryIso = getCountryIso();
Cursor cursor = db.rawQuery(
"SELECT _id, " + Phone.RAW_CONTACT_ID + ", " + Phone.NUMBER +
" FROM " + Tables.DATA +
@@ -2723,7 +2722,6 @@ import java.util.Locale;
long dataID = cursor.getLong(0);
long rawContactID = cursor.getLong(1);
String number = cursor.getString(2);
- String numberE164 = PhoneNumberUtils.formatNumberToE164(number, mCountryIso);
String normalizedNumber = PhoneNumberUtils.normalizeNumber(number);
if (!TextUtils.isEmpty(normalizedNumber)) {
phoneValues.clear();
@@ -2733,13 +2731,6 @@ import java.util.Locale;
phoneValues.put(PhoneLookupColumns.MIN_MATCH,
PhoneNumberUtils.toCallerIDMinMatch(normalizedNumber));
db.insert(Tables.PHONE_LOOKUP, null, phoneValues);
-
- if (numberE164 != null && !numberE164.equals(normalizedNumber)) {
- phoneValues.put(PhoneLookupColumns.NORMALIZED_NUMBER, numberE164);
- phoneValues.put(PhoneLookupColumns.MIN_MATCH,
- PhoneNumberUtils.toCallerIDMinMatch(numberE164));
- db.insert(Tables.PHONE_LOOKUP, null, phoneValues);
- }
}
}
} finally {
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index 4de1d13..c4b1697 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -121,10 +121,13 @@ import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
+import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
@@ -198,6 +201,10 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
/* package */ static final String PHONEBOOK_COLLATOR_NAME = "PHONEBOOK";
+ // Regex for splitting query strings - we split on any group of non-alphanumeric characters,
+ // excluding the @ symbol.
+ /* package */ static final String QUERY_TOKENIZER_REGEX = "[^\\w@]+";
+
private static final int CONTACTS = 1000;
private static final int CONTACTS_ID = 1001;
private static final int CONTACTS_LOOKUP = 1002;
@@ -3102,31 +3109,6 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
}
}
- private static class DirectoryCursorWrapper extends CursorWrapper
- implements CrossProcessCursor {
- private final CrossProcessCursor mCrossProcessCursor;
-
- public DirectoryCursorWrapper(Cursor cursor, CrossProcessCursor crossProcessCursor) {
- super(cursor);
- mCrossProcessCursor = crossProcessCursor;
- }
-
- @Override
- public void fillWindow(int pos, CursorWindow window) {
- mCrossProcessCursor.fillWindow(pos, window);
- }
-
- @Override
- public CursorWindow getWindow() {
- return mCrossProcessCursor.getWindow();
- }
-
- @Override
- public boolean onMove(int oldPosition, int newPosition) {
- return mCrossProcessCursor.onMove(oldPosition, newPosition);
- }
- }
-
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
@@ -3135,13 +3117,16 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
String directory = getQueryParameter(uri, ContactsContract.DIRECTORY_PARAM_KEY);
if (directory == null) {
- return queryLocal(uri, projection, selection, selectionArgs, sortOrder, -1);
+ return wrapCursor(uri,
+ queryLocal(uri, projection, selection, selectionArgs, sortOrder, -1));
} else if (directory.equals("0")) {
- return queryLocal(uri, projection, selection, selectionArgs, sortOrder,
- Directory.DEFAULT);
+ return wrapCursor(uri,
+ queryLocal(uri, projection, selection, selectionArgs, sortOrder,
+ Directory.DEFAULT));
} else if (directory.equals("1")) {
- return queryLocal(uri, projection, selection, selectionArgs, sortOrder,
- Directory.LOCAL_INVISIBLE);
+ return wrapCursor(uri,
+ queryLocal(uri, projection, selection, selectionArgs, sortOrder,
+ Directory.LOCAL_INVISIBLE));
}
DirectoryInfo directoryInfo = getDirectoryAuthority(directory);
@@ -3181,12 +3166,41 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
CrossProcessCursor crossProcessCursor = getCrossProcessCursor(cursor);
if (crossProcessCursor != null) {
- return new DirectoryCursorWrapper(cursor, crossProcessCursor);
+ return wrapCursor(uri, cursor);
} else {
- return matrixCursorFromCursor(cursor);
+ return matrixCursorFromCursor(wrapCursor(uri, cursor));
}
}
+ private Cursor wrapCursor(Uri uri, Cursor cursor) {
+
+ // If the cursor doesn't contain a snippet column, don't bother wrapping it.
+ if (cursor.getColumnIndex(SearchSnippetColumns.SNIPPET) < 0) {
+ return cursor;
+ }
+
+ // Parse out snippet arguments for use when snippets are retrieved from the cursor.
+ String[] args = null;
+ String snippetArgs =
+ getQueryParameter(uri, SearchSnippetColumns.SNIPPET_ARGS_PARAM_KEY);
+ if (snippetArgs != null) {
+ args = snippetArgs.split(",");
+ }
+
+ String query = uri.getLastPathSegment();
+ String startMatch = args != null && args.length > 0 ? args[0]
+ : DEFAULT_SNIPPET_ARG_START_MATCH;
+ String endMatch = args != null && args.length > 1 ? args[1]
+ : DEFAULT_SNIPPET_ARG_END_MATCH;
+ String ellipsis = args != null && args.length > 2 ? args[2]
+ : DEFAULT_SNIPPET_ARG_ELLIPSIS;
+ int maxTokens = args != null && args.length > 3 ? Integer.parseInt(args[3])
+ : DEFAULT_SNIPPET_ARG_MAX_TOKENS;
+
+ return new SnippetizingCursorWrapper(cursor, query, startMatch, endMatch, ellipsis,
+ maxTokens);
+ }
+
private CrossProcessCursor getCrossProcessCursor(Cursor cursor) {
Cursor c = cursor;
if (c instanceof CrossProcessCursor) {
@@ -3604,6 +3618,12 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
case EMAILS_FILTER: {
setTablesAndProjectionMapForData(qb, uri, projection, true);
String filterParam = null;
+
+ String primaryAccountName =
+ uri.getQueryParameter(ContactsContract.PRIMARY_ACCOUNT_NAME);
+ String primaryAccountType =
+ uri.getQueryParameter(ContactsContract.PRIMARY_ACCOUNT_TYPE);
+
if (uri.getPathSegments().size() > 3) {
filterParam = uri.getLastPathSegment();
if (TextUtils.isEmpty(filterParam)) {
@@ -3645,7 +3665,22 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
}
groupBy = Email.DATA + "," + RawContacts.CONTACT_ID;
if (sortOrder == null) {
- sortOrder = EMAIL_FILTER_SORT_ORDER;
+ // Addresses associated with primary account should be promoted.
+ if (!TextUtils.isEmpty(primaryAccountName)) {
+ StringBuilder sb2 = new StringBuilder();
+ sb2.append("(CASE WHEN " + RawContacts.ACCOUNT_NAME + "=");
+ DatabaseUtils.appendEscapedSQLString(sb2, primaryAccountName);
+ if (!TextUtils.isEmpty(primaryAccountType)) {
+ sb2.append(" AND " + RawContacts.ACCOUNT_TYPE + "=");
+ DatabaseUtils.appendEscapedSQLString(sb2, primaryAccountType);
+ }
+ sb2.append(" THEN 0 ELSE 1 END), ");
+ sb2.append(EMAIL_FILTER_SORT_ORDER);
+
+ sortOrder = sb2.toString();
+ } else {
+ sortOrder = EMAIL_FILTER_SORT_ORDER;
+ }
}
break;
}
@@ -3895,7 +3930,7 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
sortOrder, limit);
}
- qb.setStrictProjectionMap(true);
+ qb.setStrict(true);
Cursor cursor =
query(db, qb, projection, selection, selectionArgs, sortOrder, groupBy, limit);
@@ -4395,11 +4430,20 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
String phoneNumber = null;
String numberE164 = null;
+ // If the query consists of a single word, we can do snippetizing after-the-fact for a
+ // performance boost.
+ boolean singleTokenSearch = filter.split(QUERY_TOKENIZER_REGEX).length == 1;
+
if (filter.indexOf('@') != -1) {
emailAddress = mDbHelper.extractAddressFromEmailAddress(filter);
isEmailAddress = !TextUtils.isEmpty(emailAddress);
} else {
isPhoneNumber = isPhoneNumber(filter);
+ if (isPhoneNumber) {
+ phoneNumber = PhoneNumberUtils.normalizeNumber(filter);
+ numberE164 = PhoneNumberUtils.formatNumberToE164(phoneNumber,
+ mDbHelper.getCountryIso());
+ }
}
sb.append(" JOIN (SELECT " + SearchIndexColumns.CONTACT_ID + " AS snippet_contact_id");
@@ -4408,89 +4452,87 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
if (isEmailAddress) {
sb.append("ifnull(");
DatabaseUtils.appendEscapedSQLString(sb, startMatch);
- sb.append("||email_address||");
+ sb.append("||(SELECT MIN(" + Email.ADDRESS + ")");
+ sb.append(" FROM " + Tables.DATA_JOIN_RAW_CONTACTS);
+ sb.append(" WHERE " + Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID);
+ sb.append("=" + RawContacts.CONTACT_ID + " AND " + Email.ADDRESS + " LIKE ");
+ DatabaseUtils.appendEscapedSQLString(sb, filter + "%");
+ sb.append(")||");
DatabaseUtils.appendEscapedSQLString(sb, endMatch);
sb.append(",");
- appendSnippetFunction(sb, startMatch, endMatch, ellipsis, maxTokens);
+
+ // Optimization for single-token search.
+ if (singleTokenSearch) {
+ sb.append(SearchIndexColumns.CONTENT);
+ } else {
+ appendSnippetFunction(sb, startMatch, endMatch, ellipsis, maxTokens);
+ }
sb.append(")");
} else if (isPhoneNumber) {
sb.append("ifnull(");
DatabaseUtils.appendEscapedSQLString(sb, startMatch);
- sb.append("||phone_number||");
+ sb.append("||(SELECT MIN(" + Phone.NUMBER + ")");
+ sb.append(" FROM " +
+ Tables.DATA_JOIN_RAW_CONTACTS + " JOIN " + Tables.PHONE_LOOKUP);
+ sb.append(" ON " + DataColumns.CONCRETE_ID);
+ sb.append("=" + Tables.PHONE_LOOKUP + "." + PhoneLookupColumns.DATA_ID);
+ sb.append(" WHERE " + Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID);
+ sb.append("=" + RawContacts.CONTACT_ID);
+ sb.append(" AND " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '");
+ sb.append(phoneNumber);
+ sb.append("%'");
+ if (!TextUtils.isEmpty(numberE164)) {
+ sb.append(" OR " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '");
+ sb.append(numberE164);
+ sb.append("%'");
+ }
+ sb.append(")||");
DatabaseUtils.appendEscapedSQLString(sb, endMatch);
sb.append(",");
- appendSnippetFunction(sb, startMatch, endMatch, ellipsis, maxTokens);
+
+ // Optimization for single-token search.
+ if (singleTokenSearch) {
+ sb.append(SearchIndexColumns.CONTENT);
+ } else {
+ appendSnippetFunction(sb, startMatch, endMatch, ellipsis, maxTokens);
+ }
sb.append(")");
} else {
- sb.append("(CASE WHEN name_contact_id NOT NULL THEN NULL ELSE ");
- appendSnippetFunction(sb, startMatch, endMatch, ellipsis, maxTokens);
- sb.append(" END)");
+ final String normalizedFilter = NameNormalizer.normalize(filter);
+ if (!TextUtils.isEmpty(normalizedFilter)) {
+ // Optimization for single-token search.
+ if (singleTokenSearch) {
+ sb.append(SearchIndexColumns.CONTENT);
+ } else {
+ sb.append("(CASE WHEN EXISTS (SELECT 1 FROM ");
+ sb.append(Tables.RAW_CONTACTS + " AS rc INNER JOIN ");
+ sb.append(Tables.NAME_LOOKUP + " AS nl ON (rc." + RawContacts._ID);
+ sb.append("=nl." + NameLookupColumns.RAW_CONTACT_ID);
+ sb.append(") WHERE nl." + NameLookupColumns.NORMALIZED_NAME);
+ sb.append(" GLOB '" + normalizedFilter + "*' AND ");
+ sb.append("nl." + NameLookupColumns.NAME_TYPE + "=");
+ sb.append(NameLookupType.NAME_COLLATION_KEY + " AND ");
+ sb.append(Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID);
+ sb.append("=rc." + RawContacts.CONTACT_ID);
+ sb.append(") THEN NULL ELSE ");
+ appendSnippetFunction(sb, startMatch, endMatch, ellipsis, maxTokens);
+ sb.append(" END)");
+ }
+ } else {
+ sb.append("NULL");
+ }
}
sb.append(" AS " + SearchSnippetColumns.SNIPPET);
}
sb.append(" FROM " + Tables.SEARCH_INDEX);
-
- if (isEmailAddress) {
- sb.append(" LEFT OUTER JOIN " +
- "(SELECT "
- + RawContacts.CONTACT_ID + " AS email_contact_id,"
- + "MIN(" + Email.ADDRESS + ") AS email_address" +
- " FROM " + Tables.DATA_JOIN_RAW_CONTACTS +
- " WHERE " + Email.ADDRESS + " LIKE ");
- DatabaseUtils.appendEscapedSQLString(sb, filter + "%");
- sb.append(") ON (email_contact_id=snippet_contact_id)");
- } else if (isPhoneNumber) {
- phoneNumber = PhoneNumberUtils.normalizeNumber(filter);
- sb.append(" LEFT OUTER JOIN " +
- "(SELECT "
- + RawContacts.CONTACT_ID + " AS phone_contact_id,"
- + "MIN(" + Phone.NUMBER + ") AS phone_number" +
- " FROM " + Tables.DATA_JOIN_RAW_CONTACTS +
- " JOIN " + Tables.PHONE_LOOKUP +
- " ON(" + DataColumns.CONCRETE_ID + "="
- + Tables.PHONE_LOOKUP + "." + PhoneLookupColumns.DATA_ID + ")" +
- " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '");
- sb.append(phoneNumber);
- sb.append("%'");
-
- numberE164 = PhoneNumberUtils.formatNumberToE164(phoneNumber,
- mDbHelper.getCountryIso());
- if (!TextUtils.isEmpty(numberE164)) {
- sb.append(" OR " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '");
- sb.append(numberE164);
- sb.append("%'");
- }
- sb.append(" GROUP BY phone_contact_id");
- sb.append(") ON (phone_contact_id=snippet_contact_id)");
- } else {
- sb.append(" LEFT OUTER JOIN " +
- "(SELECT DISTINCT "
- + RawContacts.CONTACT_ID + " AS name_contact_id" +
- " FROM " + Tables.RAW_CONTACTS +
- " JOIN " + Tables.NAME_LOOKUP +
- " ON(" + RawContactsColumns.CONCRETE_ID + "="
- + NameLookupColumns.RAW_CONTACT_ID + ")");
-
- String normalizedFilter = NameNormalizer.normalize(filter);
- if (!TextUtils.isEmpty(normalizedFilter)) {
- sb.append(" WHERE normalized_name GLOB '");
- sb.append(normalizedFilter);
- sb.append("*' AND " + NameLookupColumns.NAME_TYPE +
- "=" + NameLookupType.NAME_COLLATION_KEY);
- } else {
- sb.append(" WHERE 0");
- }
- sb.append(") ON (name_contact_id=snippet_contact_id)");
- }
-
sb.append(" WHERE ");
sb.append(Tables.SEARCH_INDEX + " MATCH ");
if (isEmailAddress) {
DatabaseUtils.appendEscapedSQLString(sb, "\"" + sanitizeMatch(filter) + "*\"");
} else if (isPhoneNumber) {
DatabaseUtils.appendEscapedSQLString(sb,
- "\"" + sanitizeMatch(filter) + "*\" OR " + phoneNumber + "*"
+ "\"" + sanitizeMatch(filter) + "*\" OR \"" + phoneNumber + "*\""
+ (numberE164 != null ? " OR \"" + numberE164 + "\"" : ""));
} else {
DatabaseUtils.appendEscapedSQLString(sb, sanitizeMatch(filter) + "*");
@@ -4875,20 +4917,30 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
final Context context = this.getContext();
final VCardComposer composer =
new VCardComposer(context, VCardConfig.VCARD_TYPE_DEFAULT, false);
- composer.addHandler(composer.new HandlerForOutputStream(stream));
-
- // No extra checks since composer always uses restricted views
- if (!composer.init(selection, selectionArgs)) {
- Log.w(TAG, "Failed to init VCardComposer");
- return;
- }
+ Writer writer = null;
+ try {
+ writer = new BufferedWriter(new OutputStreamWriter(stream));
+ // No extra checks since composer always uses restricted views
+ if (!composer.init(selection, selectionArgs)) {
+ Log.w(TAG, "Failed to init VCardComposer");
+ return;
+ }
- while (!composer.isAfterLast()) {
- if (!composer.createOneEntry()) {
- Log.w(TAG, "Failed to output a contact.");
+ while (!composer.isAfterLast()) {
+ writer.write(composer.createOneEntry());
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "IOException: " + e);
+ } finally {
+ composer.terminate();
+ if (writer != null) {
+ try {
+ writer.close();
+ } catch (IOException e) {
+ Log.w(TAG, "IOException during closing output stream: " + e);
+ }
}
}
- composer.terminate();
}
@Override
@@ -5216,6 +5268,17 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
return null;
}
+ // Should match against the whole parameter instead of its suffix.
+ // e.g. The parameter "param" must not be found in "some_param=val".
+ if (index > 0) {
+ char prevChar = query.charAt(index - 1);
+ if (prevChar != '?' && prevChar != '&') {
+ // With "some_param=val1&param=val2", we should find second "param" occurrence.
+ index += parameterLength;
+ continue;
+ }
+ }
+
index += parameterLength;
if (queryLength == index) {
diff --git a/src/com/android/providers/contacts/GlobalSearchSupport.java b/src/com/android/providers/contacts/GlobalSearchSupport.java
index 7b9d4a6..5d0b6dc 100644
--- a/src/com/android/providers/contacts/GlobalSearchSupport.java
+++ b/src/com/android/providers/contacts/GlobalSearchSupport.java
@@ -96,6 +96,9 @@ public class GlobalSearchSupport {
+ "contacts." + Contacts.DISPLAY_NAME_PRIMARY + ", "
+ "contacts." + Contacts._ID;
+ private static final String RECENTLY_CONTACTED =
+ TIME_SINCE_LAST_CONTACTED + " < " + RECENT_CONTACTS;
+
private static class SearchSuggestion {
long contactId;
String photoUri;
@@ -182,16 +185,22 @@ public class GlobalSearchSupport {
public Cursor handleSearchSuggestionsQuery(
SQLiteDatabase db, Uri uri, String[] projection, String limit) {
+ final String searchClause;
+ final String selection;
if (uri.getPathSegments().size() <= 1) {
- return null;
+ searchClause = null;
+ selection = RECENTLY_CONTACTED;
+ } else {
+ searchClause = uri.getLastPathSegment();
+ selection = null;
}
- final String searchClause = uri.getLastPathSegment();
- if (TextUtils.isDigitsOnly(searchClause) && mContactsProvider.isPhone()) {
+ if (!TextUtils.isEmpty(searchClause) && TextUtils.isDigitsOnly(searchClause)
+ && mContactsProvider.isPhone()) {
return buildCursorForSearchSuggestionsBasedOnPhoneNumber(searchClause);
} else {
return buildCursorForSearchSuggestionsBasedOnFilter(
- db, projection, null, searchClause, limit);
+ db, projection, selection, searchClause, limit);
}
}
@@ -265,17 +274,20 @@ public class GlobalSearchSupport {
MatrixCursor cursor = new MatrixCursor(
projection != null ? projection : SEARCH_SUGGESTIONS_BASED_ON_NAME_COLUMNS);
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 + ", "
- + SearchSnippetColumns.SNIPPET + ", "
- + PRESENCE_SQL + " AS " + Contacts.CONTACT_PRESENCE +
- " FROM ");
+ + PRESENCE_SQL + " AS " + Contacts.CONTACT_PRESENCE);
+ if (haveFilter) {
+ sb.append(", " + SearchSnippetColumns.SNIPPET);
+ }
+ sb.append(" FROM ");
sb.append(getDatabaseHelper().getContactView(false));
sb.append(" AS contacts");
- if (filter != null) {
+ if (haveFilter) {
mContactsProvider.appendSearchIndexJoin(sb, filter, true,
String.valueOf(SNIPPET_START_MATCH), String.valueOf(SNIPPET_END_MATCH),
SNIPPET_ELLIPSIS, SNIPPET_MAX_TOKENS);
@@ -287,7 +299,13 @@ public class GlobalSearchSupport {
if (limit != null) {
sb.append(" LIMIT " + limit);
}
- Cursor c = db.rawQuery(sb.toString(), null);
+ Cursor c = new SnippetizingCursorWrapper(
+ db.rawQuery(sb.toString(), null),
+ haveFilter ? filter : "",
+ String.valueOf(SNIPPET_START_MATCH),
+ String.valueOf(SNIPPET_END_MATCH),
+ SNIPPET_ELLIPSIS,
+ SNIPPET_MAX_TOKENS);
SearchSuggestion suggestion = new SearchSuggestion();
suggestion.filter = filter;
try {
@@ -296,8 +314,10 @@ public class GlobalSearchSupport {
suggestion.lookupKey = c.getString(1);
suggestion.photoUri = c.getString(2);
suggestion.text1 = c.getString(3);
- suggestion.text2 = shortenSnippet(c.getString(4));
- suggestion.presence = c.isNull(5) ? -1 : c.getInt(5);
+ suggestion.presence = c.isNull(4) ? -1 : c.getInt(4);
+ if (haveFilter) {
+ suggestion.text2 = shortenSnippet(c.getString(5));
+ }
cursor.addRow(suggestion.asList(projection));
}
} finally {
diff --git a/src/com/android/providers/contacts/SnippetizingCursorWrapper.java b/src/com/android/providers/contacts/SnippetizingCursorWrapper.java
new file mode 100644
index 0000000..73fd2a3
--- /dev/null
+++ b/src/com/android/providers/contacts/SnippetizingCursorWrapper.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 2011 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.database.CrossProcessCursor;
+import android.database.Cursor;
+import android.database.CursorWindow;
+import android.database.CursorWrapper;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.SearchSnippetColumns;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Cursor wrapper for use when the results include snippets. This wrapper does special processing
+ * when the snippet field is retrieved, converting the data in that column from the raw form that is
+ * retrieved from the database into a snippet before returning (all other columns are simply passed
+ * through).
+ *
+ * Note that this wrapper implements {@link CrossProcessCursor}, but will only behave as such if the
+ * cursor it is wrapping is itself a {@link CrossProcessCursor} or another wrapper around the same.
+ */
+public class SnippetizingCursorWrapper extends CursorWrapper implements CrossProcessCursor {
+
+ // Pattern for splitting a line into tokens. This matches e-mail addresses as a single token,
+ // otherwise splitting on any group of non-alphanumeric characters.
+ Pattern SPLIT_PATTERN = Pattern.compile("([\\w-\\.]+)@((?:[\\w]+\\.)+)([a-zA-Z]{2,4})|[\\w]+");
+
+ // The cross process cursor. Only non-null if the wrapped cursor was a cross-process cursor.
+ private final CrossProcessCursor mCrossProcessCursor;
+
+ // Index of the snippet field (if any).
+ private final int mSnippetIndex;
+
+ // Parameters for snippetization.
+ private final String mQuery;
+ private final String mStartMatch;
+ private final String mEndMatch;
+ private final String mEllipsis;
+ private final int mMaxTokens;
+
+ // Whether to invoke the snippeting logic. If the query consisted of multiple tokens, the DB
+ // snippet() function was already used, so we should just return the snippet value directly.
+ private final boolean mDoSnippetizing;
+
+ /**
+ * Creates a cursor wrapper that does special handling on the snippet field (converting the
+ * raw content retrieved from the database into a snippet).
+ * @param cursor The cursor to wrap.
+ * @param query Query string.
+ * @param startMatch String to insert at the start of matches in the snippet.
+ * @param endMatch String to insert at the end of matches in the snippet.
+ * @param ellipsis Ellipsis characters to use at the start or end of the snippet if appropriate.
+ * @param maxTokens Maximum number of tokens to include in the snippet.
+ */
+ SnippetizingCursorWrapper(Cursor cursor, String query, String startMatch,
+ String endMatch, String ellipsis, int maxTokens) {
+ super(cursor);
+ mCrossProcessCursor = getCrossProcessCursor(cursor);
+ mSnippetIndex = getColumnIndex(SearchSnippetColumns.SNIPPET);
+ mQuery = query;
+ mStartMatch = startMatch;
+ mEndMatch = endMatch;
+ mEllipsis = ellipsis;
+ mMaxTokens = maxTokens;
+ mDoSnippetizing = mQuery.split(ContactsProvider2.QUERY_TOKENIZER_REGEX).length == 1;
+ }
+
+ private CrossProcessCursor getCrossProcessCursor(Cursor cursor) {
+ if (cursor instanceof CrossProcessCursor) {
+ return (CrossProcessCursor) cursor;
+ } else if (cursor instanceof CursorWrapper) {
+ return getCrossProcessCursor(((CursorWrapper) cursor).getWrappedCursor());
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public void fillWindow(int pos, CursorWindow window) {
+ if (mCrossProcessCursor != null) {
+ mCrossProcessCursor.fillWindow(pos, window);
+ } else {
+ throw new UnsupportedOperationException("Wrapped cursor is not a cross-process cursor");
+ }
+ }
+
+ @Override
+ public CursorWindow getWindow() {
+ if (mCrossProcessCursor != null) {
+ return mCrossProcessCursor.getWindow();
+ } else {
+ throw new UnsupportedOperationException("Wrapped cursor is not a cross-process cursor");
+ }
+ }
+
+ @Override
+ public boolean onMove(int oldPosition, int newPosition) {
+ if (mCrossProcessCursor != null) {
+ return mCrossProcessCursor.onMove(oldPosition, newPosition);
+ } else {
+ throw new UnsupportedOperationException("Wrapped cursor is not a cross-process cursor");
+ }
+ }
+
+ @Override
+ public String getString(int columnIndex) {
+ String columnContent = super.getString(columnIndex);
+
+ // If the snippet column is being retrieved, do our custom snippetization logic.
+ if (mDoSnippetizing && columnIndex == mSnippetIndex) {
+ // Retrieve the display name - if it includes the query term, the snippet should be
+ // left empty.
+ int displayNameIndex = super.getColumnIndex(Contacts.DISPLAY_NAME);
+ String displayName = displayNameIndex < 0 ? null : super.getString(displayNameIndex);
+ return snippetize(columnContent, displayName);
+ } else {
+ return columnContent;
+ }
+ }
+
+ /**
+ * Creates a snippet for the given content, with the following algorithm:
+ * <ul>
+ * <li>Check for the query term as a prefix of any token in the display name; if any match is
+ * found, no snippet should be computed (return null).</li>
+ * <li>Check for empty content (return null if found).</li>
+ * <li>Check for a custom snippet (phone number or email matches generate a usable snippet
+ * via a subquery)</li>
+ * <li>Check for a non-match of the query to the content - technically should never happen,
+ * but if it comes through, we'll just return null.</li>
+ * <li>Break the content into multiple lines. For each line that includes the query string:
+ * <ul>
+ * <li>Tokenize it into alphanumeric chunks.</li>
+ * <li>Do a prefix match for the query against each token.</li>
+ * <li>If there's a match, replace the token with a version with the query term surrounded
+ * by the start and end match strings.</li>
+ * <li>If this is the first token in the line that has a match, compute the start and end
+ * token positions to use for this line (which will be used to create the snippet).</li>
+ * <li>If the line just processed had a match, reassemble the tokens (with ellipses, as
+ * needed) to produce the snippet, and return it.</li>
+ * </ul>
+ * </li>
+ * </ul>
+ * @param content The content to snippetize.
+ * @param displayName Display name for the contact.
+ * @return The snippet for the content, or null if no snippet should be shown.
+ */
+ // TODO: Tokenization is done based on alphanumeric characters, which may not be appropriate for
+ // some locales (but this matches what the item view does for display).
+ private String snippetize(String content, String displayName) {
+ // If the display name already contains the query term, return empty - snippets should
+ // not be needed in that case.
+ String lowerDisplayName = displayName != null ? displayName.toLowerCase() : "";
+ String lowerQuery = mQuery.toLowerCase();
+ List<String> nameTokens = new ArrayList<String>();
+ List<Integer> nameTokenOffsets = new ArrayList<Integer>();
+ split(lowerDisplayName.trim(), nameTokens, nameTokenOffsets);
+ for (String nameToken : nameTokens) {
+ if (nameToken.startsWith(lowerQuery)) {
+ return null;
+ }
+ }
+
+ // If the content to snippetize is empty, return null.
+ if (TextUtils.isEmpty(content)) {
+ return null;
+ }
+
+ // Check to see if a custom snippet was already returned (identified by the string having no
+ // newlines and beginning and ending with the split delimiters).
+ String[] contentLines = content.split("\n");
+ if (contentLines.length == 1 && !TextUtils.isEmpty(contentLines[0])
+ && contentLines[0].startsWith(mStartMatch) && contentLines[0].endsWith(mEndMatch)) {
+ // Custom snippet was retrieved - just return it.
+ return content;
+ }
+
+ // If the content isn't a custom snippet and doesn't contain the query term, return null.
+ if (!content.toLowerCase().contains(lowerQuery)) {
+ return null;
+ }
+
+ // Locate the lines of the content that contain the query term.
+ for (String contentLine : contentLines) {
+ if (contentLine.toLowerCase().contains(lowerQuery)) {
+
+ // Line contains the query string - now search for it at the start of tokens.
+ List<String> lineTokens = new ArrayList<String>();
+ List<Integer> tokenOffsets = new ArrayList<Integer>();
+ split(contentLine.trim(), lineTokens, tokenOffsets);
+
+ // As we find matches against the query, we'll populate this list with the marked
+ // (or unchanged) tokens.
+ List<String> markedTokens = new ArrayList<String>();
+
+ int firstToken = -1;
+ int lastToken = -1;
+ for (int i = 0; i < lineTokens.size(); i++) {
+ String token = lineTokens.get(i);
+ String lowerToken = token.toLowerCase();
+ if (lowerToken.startsWith(lowerQuery)) {
+
+ // Query term matched; surround the token with match markers.
+ markedTokens.add(mStartMatch + token + mEndMatch);
+
+ // If this is the first token found with a match, mark the token
+ // positions to use for assembling the snippet.
+ if (firstToken == -1) {
+ firstToken =
+ Math.max(0, i - (int) Math.floor(Math.abs(mMaxTokens) / 2.0));
+ lastToken =
+ Math.min(lineTokens.size(), firstToken + Math.abs(mMaxTokens));
+ }
+ } else {
+ markedTokens.add(token);
+ }
+ }
+
+ // Assemble the snippet by piecing the tokens back together.
+ if (firstToken > -1) {
+ StringBuilder sb = new StringBuilder();
+ if (firstToken > 0) {
+ sb.append(mEllipsis);
+ }
+ for (int i = firstToken; i < lastToken; i++) {
+ String markedToken = markedTokens.get(i);
+ String originalToken = lineTokens.get(i);
+ sb.append(markedToken);
+ if (i < lastToken - 1) {
+ // Add the characters that appeared between this token and the next.
+ sb.append(contentLine.substring(
+ tokenOffsets.get(i) + originalToken.length(),
+ tokenOffsets.get(i + 1)));
+ }
+ }
+ if (lastToken < lineTokens.size()) {
+ sb.append(mEllipsis);
+ }
+ return sb.toString();
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Helper method for splitting a string into tokens. The lists passed in are populated with the
+ * tokens and offsets into the content of each token. The tokenization function parses e-mail
+ * addresses as a single token; otherwise it splits on any non-alphanumeric character.
+ * @param content Content to split.
+ * @param tokens List of token strings to populate.
+ * @param offsets List of offsets into the content for each token returned.
+ */
+ private void split(String content, List<String> tokens, List<Integer> offsets) {
+ Matcher matcher = SPLIT_PATTERN.matcher(content);
+ while (matcher.find()) {
+ tokens.add(matcher.group());
+ offsets.add(matcher.start());
+ }
+ }
+} \ No newline at end of file