diff options
author | blong <blong@codeaurora.org> | 2015-08-24 10:31:19 +0800 |
---|---|---|
committer | Gerrit - the friendly Code Review server <code-review@localhost> | 2015-09-05 23:51:57 -0700 |
commit | baabb38c523fbc0660cc0d573ed062df306c6a95 (patch) | |
tree | f839c892467ec48715e11a5051e5a576510c484d | |
parent | e52bc7e9db5534767f3b44f70ad4a6d25089347c (diff) | |
download | packages_providers_ContactsProvider-baabb38c523fbc0660cc0d573ed062df306c6a95.zip packages_providers_ContactsProvider-baabb38c523fbc0660cc0d573ed062df306c6a95.tar.gz packages_providers_ContactsProvider-baabb38c523fbc0660cc0d573ed062df306c6a95.tar.bz2 |
Add fuzzy search for contact number
- Add a new interface to support fuzzy search , it can search
out the contacts by any parts of number, such as for phone
number 13567854, it can be searched out by 356,785 and so on.
Change-Id: I89175ef49f70446a3f061684ae5e2abd92e92445
-rw-r--r-- | res/values/config.xml | 36 | ||||
-rw-r--r-- | src/com/android/providers/contacts/ContactsProvider2.java | 226 |
2 files changed, 259 insertions, 3 deletions
diff --git a/res/values/config.xml b/res/values/config.xml new file mode 100644 index 0000000..7d79bbe --- /dev/null +++ b/res/values/config.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2015, The Linux Foundation. All Rights Reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of The Linux Foundation nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED + WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS + BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN + IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--> + +<resources> + + <!-- If true, it supports fuzzy search for contacts number --> + <bool name="phone_number_fuzzy_search">true</bool> + +</resources>
\ No newline at end of file diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java index 94bf413..8fe1acd 100644 --- a/src/com/android/providers/contacts/ContactsProvider2.java +++ b/src/com/android/providers/contacts/ContactsProvider2.java @@ -1455,6 +1455,7 @@ public class ContactsProvider2 extends AbstractContactsProvider private Handler mBackgroundHandler; private long mLastPhotoCleanup = 0; + private boolean isPhoneNumberFuzzySearchEnabled; private FastScrollingIndexCache mFastScrollingIndexCache; @@ -1528,6 +1529,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; @@ -7414,11 +7417,228 @@ 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, |