summaryrefslogtreecommitdiffstats
path: root/src/com/android/providers/contacts/ContactsProvider2.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/providers/contacts/ContactsProvider2.java')
-rw-r--r--src/com/android/providers/contacts/ContactsProvider2.java656
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);