diff options
Diffstat (limited to 'src/com/android/providers/contacts/ContactsProvider2.java')
-rw-r--r-- | src/com/android/providers/contacts/ContactsProvider2.java | 656 |
1 files changed, 616 insertions, 40 deletions
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java index 8904dc1..870b912 100644 --- a/src/com/android/providers/contacts/ContactsProvider2.java +++ b/src/com/android/providers/contacts/ContactsProvider2.java @@ -158,10 +158,14 @@ import com.android.providers.contacts.database.MoreDatabaseUtils; import com.android.providers.contacts.util.Clock; import com.android.providers.contacts.util.ContactsPermissions; import com.android.providers.contacts.util.DbQueryUtils; +import com.android.providers.contacts.util.PreloadedContactsFileParser; import com.android.providers.contacts.util.NeededForTesting; import com.android.providers.contacts.util.UserUtils; import com.android.vcard.VCardComposer; import com.android.vcard.VCardConfig; + +import com.cyanogen.ambient.incall.CallableConstants; + import com.google.android.collect.Lists; import com.google.android.collect.Maps; import com.google.android.collect.Sets; @@ -175,6 +179,7 @@ import java.io.File; import java.io.FileDescriptor; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; @@ -193,6 +198,10 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.CountDownLatch; +import org.json.JSONException; + +import static com.cyanogen.ambient.incall.CallableConstants.ADDITIONAL_CALLABLE_MIMETYPES_PARAM_KEY; + /** * Contacts content provider. The contract between this provider and applications * is defined in {@link ContactsContract}. @@ -240,12 +249,15 @@ public class ContactsProvider2 extends AbstractContactsProvider private static final int BACKGROUND_TASK_CHANGE_LOCALE = 9; private static final int BACKGROUND_TASK_CLEANUP_PHOTOS = 10; private static final int BACKGROUND_TASK_CLEAN_DELETE_LOG = 11; + private static final int BACKGROUND_TASK_ADD_DEFAULT_CONTACT = 12; protected static final int STATUS_NORMAL = 0; protected static final int STATUS_UPGRADING = 1; protected static final int STATUS_CHANGING_LOCALE = 2; protected static final int STATUS_NO_ACCOUNTS_NO_CONTACTS = 3; + private static final String PREF_PRELOADED_CONTACTS_ADDED = "preloaded_contacts_added"; + /** Default for the maximum number of returned aggregation suggestions. */ private static final int DEFAULT_MAX_SUGGESTIONS = 5; @@ -300,6 +312,11 @@ public class ContactsProvider2 extends AbstractContactsProvider private static final String FREQUENT_ORDER_BY = DataUsageStatColumns.TIMES_USED + " DESC," + Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC"; + private static String WITHOUT_SIM_FLAG = "no_sim"; + + private boolean isWhereAppended = false; + + public static final String ADD_GROUP_MEMBERS = "add_group_members"; private static final int CONTACTS = 1000; private static final int CONTACTS_ID = 1001; @@ -355,9 +372,12 @@ public class ContactsProvider2 extends AbstractContactsProvider private static final int CALLABLES_FILTER = 3013; private static final int CONTACTABLES = 3014; private static final int CONTACTABLES_FILTER = 3015; + private static final int PHONES_ENTERPRISE = 3016; private static final int EMAILS_LOOKUP_ENTERPRISE = 3017; + private static final int CALLABLE_CONTACTS = 3018; + private static final int PHONE_LOOKUP = 4000; private static final int PHONE_LOOKUP_ENTERPRISE = 4001; @@ -532,6 +552,22 @@ public class ContactsProvider2 extends AbstractContactsProvider + " FROM " + Tables.GROUPS + " WHERE " + Groups.TITLE + "=?)))"; + private static final String CONTACTS_IN_GROUP_ID_SELECT = + Contacts._ID + " IN " + + "(SELECT DISTINCT " + + Data.CONTACT_ID + + " FROM " + Views.DATA + + " WHERE " + Data.MIMETYPE + " = '" + GroupMembership.CONTENT_ITEM_TYPE + + "' AND " + Data.DATA1 + " = ?)"; + + private static final String CONTACTS_NOT_IN_GROUP_ID_SELECT = + Contacts._ID + " NOT IN " + + "(SELECT DISTINCT " + + Data.CONTACT_ID + + " FROM " + Views.DATA + + " WHERE " + Data.MIMETYPE + " = '" + GroupMembership.CONTENT_ITEM_TYPE + + "' AND " + Data.DATA1 + " = ?)"; + /** Sql for updating DIRTY flag on multiple raw contacts */ private static final String UPDATE_RAW_CONTACT_SET_DIRTY_SQL = "UPDATE " + Tables.RAW_CONTACTS + @@ -771,6 +807,8 @@ public class ContactsProvider2 extends AbstractContactsProvider .add(Contacts.IS_USER_PROFILE) .addAll(sContactsColumns) .addAll(sContactsPresenceColumns) + .add(RawContacts.ACCOUNT_TYPE) + .add(RawContacts.ACCOUNT_NAME) .build(); /** Contains just the contacts columns */ @@ -810,6 +848,7 @@ public class ContactsProvider2 extends AbstractContactsProvider .add(Phone.LABEL) .add(Phone.IS_SUPER_PRIMARY) .add(Phone.CONTACT_ID) + .add(Phone.MIMETYPE) .add(Contacts.IS_USER_PROFILE, "NULL") .build(); @@ -917,6 +956,7 @@ public class ContactsProvider2 extends AbstractContactsProvider .addAll(sDataColumns) .addAll(sDataPresenceColumns) .addAll(sContactsColumns) + .addAll(sRawContactColumns) .addAll(sContactPresenceColumns) .addAll(sDataUsageColumns) .build(); @@ -1203,6 +1243,7 @@ public class ContactsProvider2 extends AbstractContactsProvider matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/", CONTACTS_STREQUENT); matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/filter/*", CONTACTS_STREQUENT_FILTER); + matcher.addURI(ContactsContract.AUTHORITY, "contacts/group", CONTACTS_GROUP); matcher.addURI(ContactsContract.AUTHORITY, "contacts/group/*", CONTACTS_GROUP); matcher.addURI(ContactsContract.AUTHORITY, "contacts/frequent", CONTACTS_FREQUENT); matcher.addURI(ContactsContract.AUTHORITY, "contacts/delete_usage", CONTACTS_DELETE_USAGE); @@ -1246,6 +1287,8 @@ public class ContactsProvider2 extends AbstractContactsProvider matcher.addURI(ContactsContract.AUTHORITY, "data/callables/#", CALLABLES_ID); matcher.addURI(ContactsContract.AUTHORITY, "data/callables/filter", CALLABLES_FILTER); matcher.addURI(ContactsContract.AUTHORITY, "data/callables/filter/*", CALLABLES_FILTER); + matcher.addURI(ContactsContract.AUTHORITY, "data/callables/contacts", CALLABLE_CONTACTS); + matcher.addURI(ContactsContract.AUTHORITY, "data/callables/contacts/*", CALLABLE_CONTACTS); matcher.addURI(ContactsContract.AUTHORITY, "data/contactables/", CONTACTABLES); matcher.addURI(ContactsContract.AUTHORITY, "data/contactables/filter", CONTACTABLES_FILTER); @@ -1451,6 +1494,7 @@ public class ContactsProvider2 extends AbstractContactsProvider private Handler mBackgroundHandler; private long mLastPhotoCleanup = 0; + private boolean isPhoneNumberFuzzySearchEnabled; private FastScrollingIndexCache mFastScrollingIndexCache; @@ -1524,6 +1568,8 @@ public class ContactsProvider2 extends AbstractContactsProvider profileInfo.authority = ContactsContract.AUTHORITY; mProfileProvider.attachInfo(getContext(), profileInfo); mProfileHelper = mProfileProvider.getDatabaseHelper(getContext()); + isPhoneNumberFuzzySearchEnabled = getContext().getResources().getBoolean( + R.bool.phone_number_fuzzy_search); // Initialize the pre-authorized URI duration. mPreAuthorizedUriDuration = DEFAULT_PREAUTHORIZED_URI_EXPIRATION; @@ -1537,6 +1583,7 @@ public class ContactsProvider2 extends AbstractContactsProvider scheduleBackgroundTask(BACKGROUND_TASK_OPEN_WRITE_ACCESS); scheduleBackgroundTask(BACKGROUND_TASK_CLEANUP_PHOTOS); scheduleBackgroundTask(BACKGROUND_TASK_CLEAN_DELETE_LOG); + scheduleBackgroundTask(BACKGROUND_TASK_ADD_DEFAULT_CONTACT); return true; } @@ -1777,9 +1824,53 @@ public class ContactsProvider2 extends AbstractContactsProvider DeletedContactsTableUtil.deleteOldLogs(db); break; } + + case BACKGROUND_TASK_ADD_DEFAULT_CONTACT: { + if (shouldAttemptPreloadingContacts()) { + try { + InputStream inputStream = getContext().getResources().openRawResource( + R.raw.preloaded_contacts); + PreloadedContactsFileParser pcfp = new + PreloadedContactsFileParser(inputStream); + ArrayList<ContentProviderOperation> cpOperations = pcfp.parseForContacts(); + if (cpOperations == null) break; + + getContext().getContentResolver().applyBatch(ContactsContract.AUTHORITY, + cpOperations); + // persist the completion of the transaction + onPreloadingContactsComplete(); + + } catch (NotFoundException nfe) { + System.out.println(); + nfe.printStackTrace(); + } catch (JSONException e) { + e.printStackTrace(); + } catch (RemoteException e) { + e.printStackTrace(); + } catch (OperationApplicationException e) { + e.printStackTrace(); + } + } + + break; + } + } } + private boolean shouldAttemptPreloadingContacts() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + return getContext().getResources().getBoolean(R.bool.config_preload_contacts) && + !prefs.getBoolean(PREF_PRELOADED_CONTACTS_ADDED, false); + } + + private void onPreloadingContactsComplete() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(PREF_PRELOADED_CONTACTS_ADDED, true); + editor.commit(); + } + public void onLocaleChanged() { if (mProviderStatus != STATUS_NORMAL && mProviderStatus != STATUS_NO_ACCOUNTS_NO_CONTACTS) { @@ -5351,13 +5442,43 @@ public class ContactsProvider2 extends AbstractContactsProvider filterParam = uri.getLastPathSegment(); } - // If the query consists of a single word, we can do snippetizing after-the-fact for - // a performance boost. Otherwise, we can't defer. + // If the query consists of a single word, we can do snippetizing + // after-the-fact for a performance boost. Otherwise, we can't defer. snippetDeferred = isSingleWordQuery(filterParam) && deferredSnipRequested && snippetNeeded(projection); setTablesAndProjectionMapForContactsWithSnippet( qb, uri, projection, filterParam, directoryId, snippetDeferred); + long groupId = -1; + try { + groupId = Long.parseLong(uri.getQueryParameter(Groups._ID)); + } catch (Exception exception) { + groupId = -1; + } + if (groupId != -1) { + StringBuilder groupBuilder = new StringBuilder(); + if (uri.getBooleanQueryParameter(ADD_GROUP_MEMBERS, false)) { + // filter all the contacts that are NOT assigned to the + // group whose id is 'groupId' + groupBuilder.append(Contacts._ID + " NOT IN (" + " SELECT DISTINCT " + + Data.CONTACT_ID + + " FROM " + Views.DATA + " WHERE " + Data.MIMETYPE + " = '" + + GroupMembership.CONTENT_ITEM_TYPE + "' AND " + Data.DATA1 + + " = " + groupId + ")"); + } else { + // filter all the contacts that are assigned to the + // group whose id is 'groupId' + groupBuilder.append(Contacts._ID + " IN (" + " SELECT DISTINCT " + + Data.CONTACT_ID + + " FROM " + Views.DATA + " WHERE " + Data.MIMETYPE + " = '" + + GroupMembership.CONTENT_ITEM_TYPE + "' AND " + Data.DATA1 + + " = " + groupId + ")"); + } + if (isWhereAppended) { + qb.appendWhere(" AND "); + } + qb.appendWhere(groupBuilder.toString()); + } break; } @@ -5421,12 +5542,17 @@ public class ContactsProvider2 extends AbstractContactsProvider mDbHelper.get().getMimeTypeId(Phone.CONTENT_ITEM_TYPE); final long sipMimeTypeId = mDbHelper.get().getMimeTypeId(SipAddress.CONTENT_ITEM_TYPE); + final String additionalCallableMimeTypes = getCallableMimeTypesFromUri(uri); + String mimeTypeIdClauses = phoneMimeTypeId + ", " + sipMimeTypeId; + if (!TextUtils.isEmpty(additionalCallableMimeTypes)) { + mimeTypeIdClauses += ", " + additionalCallableMimeTypes; + } qb.appendWhere(DbQueryUtils.concatenateClauses( selection, "(" + Contacts.STARRED + "=1", DataColumns.MIMETYPE_ID + " IN (" + - phoneMimeTypeId + ", " + sipMimeTypeId + ")) AND (" + + mimeTypeIdClauses + ")) AND (" + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY + ")")); starredInnerQuery = qb.buildQuery(subProjection, null, null, null, Data.IS_SUPER_PRIMARY + " DESC," + SORT_BY_DATA_USAGE, null); @@ -5455,7 +5581,7 @@ public class ContactsProvider2 extends AbstractContactsProvider selection, "(" + Contacts.STARRED + "=0 OR " + Contacts.STARRED + " IS NULL", DataColumns.MIMETYPE_ID + " IN (" + - phoneMimeTypeId + ", " + sipMimeTypeId + ")) AND (" + + mimeTypeIdClauses + ")) AND (" + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY + ")")); frequentInnerQuery = qb.buildQuery(subProjection, null, null, null, SORT_BY_DATA_USAGE, "25"); @@ -5539,7 +5665,23 @@ public class ContactsProvider2 extends AbstractContactsProvider case CONTACTS_GROUP: { setTablesAndProjectionMapForContacts(qb, projection); - if (uri.getPathSegments().size() > 2) { + appendLocalDirectoryAndAccountSelectionIfNeeded(qb, directoryId, uri); + long groupId = -1; + try { + groupId = Long.parseLong(uri.getQueryParameter(Groups._ID)); + } catch (Exception exception) { + groupId = -1; + } + if (groupId != -1) { + qb.appendWhere(" AND "); + if (uri.getBooleanQueryParameter(ADD_GROUP_MEMBERS, false)) { + qb.appendWhere(CONTACTS_NOT_IN_GROUP_ID_SELECT); + } else { + qb.appendWhere(CONTACTS_IN_GROUP_ID_SELECT); + } + selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(groupId)); + } else if (uri.getPathSegments().size() > 2) { + qb.appendWhere(" AND "); qb.appendWhere(CONTACTS_IN_GROUP_SELECT); String groupMimeTypeId = String.valueOf( mDbHelper.get().getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)); @@ -5679,10 +5821,13 @@ public class ContactsProvider2 extends AbstractContactsProvider DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForPhone(); final String mimeTypeIsSipExpression = DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForSip(); + setTablesAndProjectionMapForData(qb, uri, projection, false); if (match == CALLABLES) { - qb.appendWhere(" AND ((" + mimeTypeIsPhoneExpression + - ") OR (" + mimeTypeIsSipExpression + "))"); + String appendWhere = " AND ((" + mimeTypeIsPhoneExpression + ") OR (" + + mimeTypeIsSipExpression + ")" + + appendMimeTypeQueryParameters(uri) + ")"; + qb.appendWhere(appendWhere); } else { qb.appendWhere(" AND " + mimeTypeIsPhoneExpression); } @@ -5704,6 +5849,32 @@ public class ContactsProvider2 extends AbstractContactsProvider } break; } + case CALLABLE_CONTACTS: { + final String mimeTypeIsPhoneExpression = + DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForPhone(); + final String mimeTypeIsSipExpression = + DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForSip(); + + setTablesAndProjectionMapForData(qb, uri, projection, false); + + String appendWhere = " AND ((" + mimeTypeIsPhoneExpression + ") OR (" + + mimeTypeIsSipExpression + ")" + + appendMimeTypeQueryParameters(uri) + ")"; + qb.appendWhere(appendWhere); + + final boolean removeDuplicates = readBooleanQueryParameter( + uri, ContactsContract.REMOVE_DUPLICATE_ENTRIES, false); + if (removeDuplicates) { + groupBy = RawContacts.CONTACT_ID; + + // In this case, because we dedupe phone numbers, the address book indexer needs + // to take it into account too. (Otherwise headers will appear in wrong + // positions.) + // So use count(distinct CONTACT_ID) instead of count(*). + addressBookIndexerCountExpression = "DISTINCT " + RawContacts.CONTACT_ID; + } + break; + } case PHONES_ID: case CALLABLES_ID: { @@ -5735,8 +5906,10 @@ public class ContactsProvider2 extends AbstractContactsProvider DataUsageStatColumns.USAGE_TYPE_INT_CALL); setTablesAndProjectionMapForData(qb, uri, projection, true, typeInt); if (match == CALLABLES_FILTER) { - qb.appendWhere(" AND ((" + mimeTypeIsPhoneExpression + - ") OR (" + mimeTypeIsSipExpression + "))"); + String appendWhere = " AND ((" + mimeTypeIsPhoneExpression + ") OR (" + + mimeTypeIsSipExpression + ")" + + appendMimeTypeQueryParameters(uri) + ")"; + qb.appendWhere(appendWhere); } else { qb.appendWhere(" AND " + mimeTypeIsPhoneExpression); } @@ -6210,23 +6383,34 @@ public class ContactsProvider2 extends AbstractContactsProvider String number = uri.getPathSegments().size() > 1 ? uri.getLastPathSegment() : ""; - String numberE164 = PhoneNumberUtils.formatNumberToE164( - number, mDbHelper.get().getCurrentCountryIso()); - String normalizedNumber = PhoneNumberUtils.normalizeNumber(number); - mDbHelper.get().buildPhoneLookupAndContactQuery( - qb, normalizedNumber, numberE164); - qb.setProjectionMap(sPhoneLookupProjectionMap); - - // removeNonStarMatchesFromCursor() requires the cursor to contain - // PhoneLookup.NUMBER. Therefore, if the projection explicitly omits it, extend - // the projection. - String[] projectionWithNumber = projection; - if (projection != null - && !ArrayUtils.contains(projection,PhoneLookup.NUMBER)) { - projectionWithNumber = ArrayUtils.appendElement( - String.class, projection, PhoneLookup.NUMBER); + + boolean isPhoneNumber = isPhoneNumber(number); + + String[] projectionWithNumber; + if (isPhoneNumber) { + String numberE164 = PhoneNumberUtils.formatNumberToE164( + number, mDbHelper.get().getCurrentCountryIso()); + String normalizedNumber = PhoneNumberUtils.normalizeNumber(number); + mDbHelper.get().buildPhoneLookupAndContactQuery( + qb, normalizedNumber, numberE164); + qb.setProjectionMap(sPhoneLookupProjectionMap); + + // removeNonStarMatchesFromCursor() requires the cursor to contain + // PhoneLookup.NUMBER. Therefore, if the projection explicitly omits it, extend + // the projection. + projectionWithNumber = projection; + if (projection != null + && !ArrayUtils.contains(projection,PhoneLookup.NUMBER)) { + projectionWithNumber = ArrayUtils.appendElement( + String.class, projection, PhoneLookup.NUMBER); + } + } else { + mDbHelper.get().buildDataLookupAndContactQuery(qb, number); + projectionWithNumber = new String[0]; + sortOrder = null; } + // Peek at the results of the first query (which attempts to use fully // normalized and internationalized numbers for comparison). If no results // were returned, fall back to using the SQLite function @@ -6238,8 +6422,12 @@ public class ContactsProvider2 extends AbstractContactsProvider try { if (cursor.getCount() > 0) { foundResult = true; - return PhoneLookupWithStarPrefix - .removeNonStarMatchesFromCursor(number, cursor); + if (isPhoneNumber) { + return PhoneLookupWithStarPrefix + .removeNonStarMatchesFromCursor(number, cursor); + } else { + return cursor; + } } // Use the fall-back lookup method. @@ -6492,6 +6680,45 @@ public class ContactsProvider2 extends AbstractContactsProvider return cursor; } + private String appendMimeTypeQueryParameters(Uri uri) { + final String mimeTypesQueryParameter = + getQueryParameter(uri, ADDITIONAL_CALLABLE_MIMETYPES_PARAM_KEY); + StringBuilder appendWhere = new StringBuilder(); + if (!TextUtils.isEmpty(mimeTypesQueryParameter)) { + List<String> mimeTypesQueryParameterList = + Arrays.asList(mimeTypesQueryParameter.split("\\s*,\\s*")); + if (mimeTypesQueryParameterList != null && !mimeTypesQueryParameterList.isEmpty()) { + // Parse URI + for (String mimeType : mimeTypesQueryParameterList) { + long mimeTypeId = mDbHelper.get().getMimeTypeId(mimeType); + String mimeTypeIsExpression = DataColumns.MIMETYPE_ID + "=" + mimeTypeId; + appendWhere.append(" OR (" + mimeTypeIsExpression + ")"); + } + } + } + return appendWhere.toString(); + } + + private String getCallableMimeTypesFromUri(Uri uri) { + final String mimeTypesQueryParameter = + getQueryParameter(uri, ADDITIONAL_CALLABLE_MIMETYPES_PARAM_KEY); + StringBuilder mimeTypeIds = new StringBuilder(); + if (!TextUtils.isEmpty(mimeTypesQueryParameter)) { + List<String> mimeTypesQueryParameterList = + Arrays.asList(mimeTypesQueryParameter.split("\\s*,\\s*")); + if (mimeTypesQueryParameterList != null && !mimeTypesQueryParameterList.isEmpty()) { + // Parse URI + for (String mimeType : mimeTypesQueryParameterList) { + if (!TextUtils.isEmpty(mimeTypeIds.toString())) { + mimeTypeIds.append(", "); + } + mimeTypeIds.append(mDbHelper.get().getMimeTypeId(mimeType)); + } + } + } + return mimeTypeIds.toString(); + } + // Rewrites query sort orders using SORT_KEY_{PRIMARY, ALTERNATIVE} // to use PHONEBOOK_BUCKET_{PRIMARY, ALTERNATIVE} as primary key; all // other sort orders are returned unchanged. Preserves ordering @@ -7320,9 +7547,63 @@ public class ContactsProvider2 extends AbstractContactsProvider private void setTablesAndProjectionMapForContactsWithSnippet(SQLiteQueryBuilder qb, Uri uri, String[] projection, String filter, long directoryId, boolean deferSnippeting) { + isWhereAppended = false; StringBuilder sb = new StringBuilder(); sb.append(Views.CONTACTS); + /* Do not show contacts when SIM card is disabled for CONTACTS_FILTER */ + StringBuilder sbWhere = new StringBuilder(); + String withoutSim = getQueryParameter(uri, WITHOUT_SIM_FLAG ); + if ("true".equals(withoutSim)) { + final long[] accountId = getAccountIdWithoutSim(uri); + if (accountId == null) { + // No such account. + sbWhere.setLength(0); + sbWhere.append("(1=2)"); + } else { + if (accountId.length > 0) { + sbWhere.append(" (" + Contacts._ID + " not IN (" + "SELECT " + + RawContacts.CONTACT_ID + " FROM " + + Tables.RAW_CONTACTS + " WHERE " + + RawContacts.CONTACT_ID + " not NULL AND ( "); + for (int i = 0; i < accountId.length; i++) { + sbWhere.append(RawContactsColumns.ACCOUNT_ID + "=" + + accountId[i]); + if (i != accountId.length - 1) { + sbWhere.append(" OR "); + } + } + sbWhere.append(")))"); + } + } + } else { + final AccountWithDataSet accountWithDataSet = getAccountWithDataSetFromUri(uri); + // Accounts are valid by only checking one parameter, since we've + // already ruled out partial accounts. + final boolean validAccount = !TextUtils.isEmpty(accountWithDataSet.getAccountName()); + if (validAccount) { + final Long accountId = mDbHelper.get().getAccountIdOrNull(accountWithDataSet); + if (accountId != null) { + sbWhere.append(" INNER JOIN (SELECT " + + RawContacts.CONTACT_ID + + " AS raw_contact_contact_id FROM " + + Tables.RAW_CONTACTS + " WHERE " + + RawContactsColumns.ACCOUNT_ID + " = " + + accountId + + ") ON raw_contact_contact_id = " + Contacts._ID); + } + } + } + + if (!TextUtils.isEmpty(sbWhere.toString())) { + if ("true".equals(withoutSim)) { + qb.appendWhere(sbWhere.toString()); + isWhereAppended = true; + } else { + sb.append(sbWhere.toString()); + } + } + if (filter != null) { filter = filter.trim(); } @@ -7359,13 +7640,230 @@ public class ContactsProvider2 extends AbstractContactsProvider int maxTokens = args != null && args.length > 3 ? Integer.parseInt(args[3]) : DEFAULT_SNIPPET_ARG_MAX_TOKENS; - appendSearchIndexJoin( - sb, filter, true, startMatch, endMatch, ellipsis, maxTokens, deferSnippeting); + if (isPhoneNumberFuzzySearchEnabled) { + appendSearchIndexJoinForFuzzySearch(sb, filter, true, + startMatch, endMatch, ellipsis, maxTokens, deferSnippeting); + } else { + appendSearchIndexJoin(sb, filter, true, startMatch, endMatch, + ellipsis, maxTokens, deferSnippeting); + } } else { - appendSearchIndexJoin(sb, filter, false, null, null, null, 0, false); + if (isPhoneNumberFuzzySearchEnabled) { + appendSearchIndexJoinForFuzzySearch(sb, filter, false, null, + null, null, 0, false); + } else { + appendSearchIndexJoin(sb, filter, false, null, null, null, 0, + false); + } } } + public void appendSearchIndexJoinForFuzzySearch(StringBuilder sb, String filter, + boolean snippetNeeded, String startMatch, String endMatch, String ellipsis, + int maxTokens, boolean deferSnippeting) { + boolean isEmailAddress = false; + String emailAddress = null; + boolean isPhoneNumber = false; + String phoneNumber = null; + String numberE164 = null; + + + if (filter.indexOf('@') != -1) { + emailAddress = mDbHelper.get().extractAddressFromEmailAddress(filter); + isEmailAddress = !TextUtils.isEmpty(emailAddress); + } else { + isPhoneNumber = isPhoneNumber(filter); + if (isPhoneNumber) { + phoneNumber = PhoneNumberUtils.normalizeNumber(filter); + numberE164 = PhoneNumberUtils.formatNumberToE164(phoneNumber, + mDbHelper.get().getCurrentCountryIso()); + } + } + + final String SNIPPET_CONTACT_ID = "snippet_contact_id"; + sb.append(" JOIN (SELECT " + SearchIndexColumns.CONTACT_ID + " AS " + SNIPPET_CONTACT_ID); + if (snippetNeeded) { + sb.append(", "); + if (isEmailAddress) { + sb.append("ifnull("); + if (!deferSnippeting) { + // Add the snippet marker only when we're really creating snippet. + DatabaseUtils.appendEscapedSQLString(sb, startMatch); + sb.append("||"); + } + 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(")"); + if (!deferSnippeting) { + sb.append("||"); + DatabaseUtils.appendEscapedSQLString(sb, endMatch); + } + sb.append(","); + + if (deferSnippeting) { + sb.append(SearchIndexColumns.CONTENT); + } else { + appendSnippetFunction(sb, startMatch, endMatch, ellipsis, maxTokens); + } + sb.append(")"); + } else if (isPhoneNumber) { + sb.append("ifnull("); + if (!deferSnippeting) { + // Add the snippet marker only when we're really creating snippet. + DatabaseUtils.appendEscapedSQLString(sb, startMatch); + sb.append("||"); + } + 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("%'"); + sb.append("))"); + if (! deferSnippeting) { + sb.append("||"); + DatabaseUtils.appendEscapedSQLString(sb, endMatch); + } + sb.append(","); + + if (deferSnippeting) { + sb.append(SearchIndexColumns.CONTENT); + } else { + appendSnippetFunction(sb, startMatch, endMatch, ellipsis, maxTokens); + } + sb.append(")"); + } else { + final String normalizedFilter = NameNormalizer.normalize(filter); + if (!TextUtils.isEmpty(normalizedFilter)) { + if (deferSnippeting) { + 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 " + SearchSnippets.SNIPPET); + } + + sb.append(" FROM " + Tables.SEARCH_INDEX); + sb.append(" WHERE "); + if (isPhoneNumber) { + sb.append(Tables.SEARCH_INDEX + " MATCH '"); + // normalized version of the phone number (phoneNumber can only have + // + and digits) + final String phoneNumberCriteria = " OR tokens:" + phoneNumber + + "*"; + + // international version of this number (numberE164 can only have + + // and digits) + final String numberE164Criteria = (numberE164 != null && !TextUtils + .equals(numberE164, phoneNumber)) ? " OR tokens:" + + numberE164 + "*" : ""; + + // combine all criteria + final String commonCriteria = phoneNumberCriteria + + numberE164Criteria; + + // search in content + sb.append(SearchIndexManager.getFtsMatchQuery(filter, + FtsQueryBuilder.getDigitsQueryBuilder(commonCriteria))); + sb.append("' AND " + SNIPPET_CONTACT_ID + " IN " + + Tables.DEFAULT_DIRECTORY); + if (snippetNeeded) { + // only support fuzzy search when there is snippet column and + // the filter is phone number! + sb.append(" UNION SELECT " + SearchIndexColumns.CONTACT_ID + " AS " + + SNIPPET_CONTACT_ID); + sb.append(", "); + if (!deferSnippeting) { + // Add the snippet marker only when we're really creating snippet. + DatabaseUtils.appendEscapedSQLString(sb, startMatch); + sb.append("||"); + } + 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("%'"); + sb.append("))"); + if (!deferSnippeting) { + sb.append("||"); + DatabaseUtils.appendEscapedSQLString(sb, endMatch); + } + sb.append(" AS " + SearchSnippets.SNIPPET); + sb.append(" FROM " + Tables.SEARCH_INDEX); + sb.append(" WHERE " + SearchSnippets.SNIPPET + " IS NOT NULL "); + sb.append(" AND " + SNIPPET_CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY + ")"); + } else { + sb.append(")"); + } + } else { + sb.append(Tables.SEARCH_INDEX + " MATCH '"); + if (isEmailAddress) { + // we know that the emailAddress contains a @. This phrase search should be + // scoped against "content:" only, but unfortunately SQLite doesn't support + // phrases and scoped columns at once. This is fine in this case however, because: + // We can't erroneously match against name, as it is all-hex (so the @ can't match) + // We can't match against tokens, because phone-numbers can't contain @ + final String sanitizedEmailAddress = + emailAddress == null ? "" : sanitizeMatch(emailAddress); + sb.append("\""); + sb.append(sanitizedEmailAddress); + sb.append("*\""); + } else if (isPhoneNumber) { + // normalized version of the phone number (phoneNumber can only have + and digits) + final String phoneNumberCriteria = " OR tokens:" + phoneNumber + "*"; + + // international version of this number (numberE164 can only have + and digits) + final String numberE164Criteria = + (numberE164 != null && !TextUtils.equals(numberE164, phoneNumber)) + ? " OR tokens:" + numberE164 + "*" + : ""; + + // combine all criteria + final String commonCriteria = + phoneNumberCriteria + numberE164Criteria; + + // search in content + sb.append(SearchIndexManager.getFtsMatchQuery(filter, + FtsQueryBuilder.getDigitsQueryBuilder(commonCriteria))); + } else { + // general case: not a phone number, not an email-address + sb.append(SearchIndexManager.getFtsMatchQuery(filter, + FtsQueryBuilder.SCOPED_NAME_NORMALIZING)); + } + // Omit results in "Other Contacts". + sb.append("' AND " + SNIPPET_CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY + ")"); + } + sb.append(" ON (" + Contacts._ID + "=" + SNIPPET_CONTACT_ID + ")"); + } + public void appendSearchIndexJoin(StringBuilder sb, String filter, boolean snippetNeeded, String startMatch, String endMatch, String ellipsis, int maxTokens, boolean deferSnippeting) { @@ -7747,22 +8245,50 @@ public class ContactsProvider2 extends AbstractContactsProvider sb.append("(1)"); } - final AccountWithDataSet accountWithDataSet = getAccountWithDataSetFromUri(uri); - // Accounts are valid by only checking one parameter, since we've - // already ruled out partial accounts. - final boolean validAccount = !TextUtils.isEmpty(accountWithDataSet.getAccountName()); - if (validAccount) { - final Long accountId = mDbHelper.get().getAccountIdOrNull(accountWithDataSet); + String withoutSim = getQueryParameter(uri, WITHOUT_SIM_FLAG ); + if ("true".equals(withoutSim)) { + final long[] accountId = getAccountIdWithoutSim(uri); + if (accountId == null) { // No such account. sb.setLength(0); sb.append("(1=2)"); } else { - sb.append( - " AND (" + Contacts._ID + " IN (" + - "SELECT " + RawContacts.CONTACT_ID + " FROM " + Tables.RAW_CONTACTS + - " WHERE " + RawContactsColumns.ACCOUNT_ID + "=" + accountId.toString() + - "))"); + if (accountId.length > 0) { + sb.append( + " AND (" + Contacts._ID + " not IN (" + + "SELECT " + RawContacts.CONTACT_ID + " FROM " + + Tables.RAW_CONTACTS + + " WHERE " + RawContacts.CONTACT_ID + " not NULL AND ( "); + for (int i = 0; i < accountId.length; i++) { + sb.append(RawContactsColumns.ACCOUNT_ID + "=" + + accountId[i]); + if (i != accountId.length - 1) { + sb.append(" or "); + } + } + sb.append(")))"); + } + } + } + else { + final AccountWithDataSet accountWithDataSet = getAccountWithDataSetFromUri(uri); + // Accounts are valid by only checking one parameter, since we've + // already ruled out partial accounts. + final boolean validAccount = !TextUtils.isEmpty(accountWithDataSet.getAccountName()); + if (validAccount) { + final Long accountId = mDbHelper.get().getAccountIdOrNull(accountWithDataSet); + if (accountId == null) { + // No such account. + sb.setLength(0); + sb.append("(1=2)"); + } else { + sb.append( + " AND (" + Contacts._ID + " IN (" + + "SELECT " + RawContacts.CONTACT_ID + " FROM " + Tables.RAW_CONTACTS + + " WHERE " + RawContactsColumns.ACCOUNT_ID + "=" + accountId.toString() + + "))"); + } } } qb.appendWhere(sb.toString()); @@ -7812,6 +8338,56 @@ public class ContactsProvider2 extends AbstractContactsProvider } } + private long[] getAccountIdWithoutSim(Uri uri) { + final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE); + final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME); + Cursor c = null; + SQLiteDatabase db = mContactsHelper.getWritableDatabase(); + long[] accountId = null; + try { + if (null != accountType) { + c = db.query(Tables.ACCOUNTS, + new String[] { AccountsColumns._ID }, + AccountsColumns.ACCOUNT_TYPE + "=?", + new String[] { String.valueOf(accountType) }, null, + null, null); + } else if (!TextUtils.isEmpty(accountName)) { + String[] names = accountName.split(","); + int nameCount = names.length; + String where = AccountsColumns.ACCOUNT_NAME + "=?"; + StringBuilder selection = new StringBuilder(); + String[] selectionArgs = new String[nameCount]; + for (int i = 0; i < nameCount; i++) { + selection.append(where); + if (i != nameCount - 1) { + selection.append(" OR "); + } + selectionArgs[i] = names[i]; + } + c = db.query(Tables.ACCOUNTS, + new String[] { AccountsColumns._ID }, + selection.toString(), + selectionArgs, null, + null, null); + } + + if (c != null) { + accountId = new long[c.getCount()]; + + for (int i = 0; i < c.getCount(); i++) { + if (c.moveToNext()) { + accountId[c.getPosition()] = c.getInt(0); + } + } + } + } finally { + if (c != null) { + c.close(); + } + } + return accountId; + } + private AccountWithDataSet getAccountWithDataSetFromUri(Uri uri) { final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME); final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE); |