diff options
Diffstat (limited to 'src')
4 files changed, 517 insertions, 20 deletions
diff --git a/src/com/android/dialer/dialpad/SmartDialCache.java b/src/com/android/dialer/dialpad/SmartDialCache.java index 79cc72763..7a11b15d8 100644 --- a/src/com/android/dialer/dialpad/SmartDialCache.java +++ b/src/com/android/dialer/dialpad/SmartDialCache.java @@ -19,22 +19,27 @@ package com.android.dialer.dialpad; import static com.android.dialer.dialpad.SmartDialAdapter.LOG_TAG; import android.content.Context; -import android.database.ContentObserver; +import android.content.SharedPreferences; import android.database.Cursor; import android.net.Uri; -import android.os.Handler; +import android.preference.PreferenceManager; import android.provider.ContactsContract; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.Data; import android.provider.ContactsContract.Directory; +import android.telephony.TelephonyManager; +import android.text.TextUtils; import android.util.Log; import com.android.contacts.common.util.StopWatch; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import java.util.Comparator; +import java.util.HashSet; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; /** @@ -134,12 +139,23 @@ public class SmartDialCache { + Data.IS_PRIMARY + " DESC"; } + // Static set used to determine which countries use NANP numbers + public static Set<String> sNanpCountries = null; + private SmartDialTrie mContactsCache; private static AtomicInteger mCacheStatus; private final int mNameDisplayOrder; private final Context mContext; private final static Object mLock = new Object(); + /** The country code of the user's sim card obtained by calling getSimCountryIso*/ + private static final String PREF_USER_SIM_COUNTRY_CODE = + "DialtactsActivity_user_sim_country_code"; + private static final String PREF_USER_SIM_COUNTRY_CODE_DEFAULT = null; + + private static String sUserSimCountryCode = PREF_USER_SIM_COUNTRY_CODE_DEFAULT; + private static boolean sUserInNanpRegion = false; + public static final int CACHE_NEEDS_RECACHE = 1; public static final int CACHE_IN_PROGRESS = 2; public static final int CACHE_COMPLETED = 3; @@ -151,6 +167,27 @@ public class SmartDialCache { Preconditions.checkNotNull(context, "Context must not be null"); mContext = context.getApplicationContext(); mCacheStatus = new AtomicInteger(CACHE_NEEDS_RECACHE); + + final TelephonyManager manager = (TelephonyManager) context.getSystemService( + Context.TELEPHONY_SERVICE); + if (manager != null) { + sUserSimCountryCode = manager.getSimCountryIso(); + } + + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + + if (sUserSimCountryCode != null) { + // Update shared preferences with the latest country obtained from getSimCountryIso + prefs.edit().putString(PREF_USER_SIM_COUNTRY_CODE, sUserSimCountryCode).apply(); + } else { + // Couldn't get the country from getSimCountryIso. Maybe we are in airplane mode. + // Try to load the settings, if any from SharedPreferences. + sUserSimCountryCode = prefs.getString(PREF_USER_SIM_COUNTRY_CODE, + PREF_USER_SIM_COUNTRY_CODE_DEFAULT); + } + + sUserInNanpRegion = isCountryNanp(sUserSimCountryCode); + } private static SmartDialCache instance; @@ -200,7 +237,7 @@ public class SmartDialCache { return; } final SmartDialTrie cache = new SmartDialTrie( - SmartDialNameMatcher.LATIN_LETTERS_TO_DIGITS); + SmartDialNameMatcher.LATIN_LETTERS_TO_DIGITS, sUserInNanpRegion); try { c.moveToPosition(-1); int affinityCount = 0; @@ -259,8 +296,7 @@ public class SmartDialCache { // the contacts again. if (mContactsCache == null) { cacheContacts(mContext); - return (mContactsCache == null) ? new SmartDialTrie( - SmartDialNameMatcher.LATIN_LETTERS_TO_DIGITS) : mContactsCache; + return (mContactsCache == null) ? new SmartDialTrie() : mContactsCache; } else { // After waiting for the lock on mLock to be released, mContactsCache is now // non-null due to the completion of the caching thread (Scenario 2). Go ahead @@ -313,4 +349,54 @@ public class SmartDialCache { } } + + public boolean getUserInNanpRegion() { + return sUserInNanpRegion; + } + + /** + * Indicates whether the given country uses NANP numbers + * + * @param country ISO 3166 country code (case doesn't matter) + * @return True if country uses NANP numbers (e.g. US, Canada), false otherwise + */ + @VisibleForTesting + static boolean isCountryNanp(String country) { + if (TextUtils.isEmpty(country)) { + return false; + } + if (sNanpCountries == null) { + sNanpCountries = initNanpCountries(); + } + return sNanpCountries.contains(country.toUpperCase()); + } + + private static Set<String> initNanpCountries() { + final HashSet<String> result = new HashSet<String>(); + result.add("US"); // United States + result.add("CA"); // Canada + result.add("AS"); // American Samoa + result.add("AI"); // Anguilla + result.add("AG"); // Antigua and Barbuda + result.add("BS"); // Bahamas + result.add("BB"); // Barbados + result.add("BM"); // Bermuda + result.add("VG"); // British Virgin Islands + result.add("KY"); // Cayman Islands + result.add("DM"); // Dominica + result.add("DO"); // Dominican Republic + result.add("GD"); // Grenada + result.add("GU"); // Guam + result.add("JM"); // Jamaica + result.add("PR"); // Puerto Rico + result.add("MS"); // Montserrat + result.add("MP"); // Northern Mariana Islands + result.add("KN"); // Saint Kitts and Nevis + result.add("LC"); // Saint Lucia + result.add("VC"); // Saint Vincent and the Grenadines + result.add("TT"); // Trinidad and Tobago + result.add("TC"); // Turks and Caicos Islands + result.add("VI"); // U.S. Virgin Islands + return result; + } } diff --git a/src/com/android/dialer/dialpad/SmartDialLoaderTask.java b/src/com/android/dialer/dialpad/SmartDialLoaderTask.java index c5fc65761..08c526503 100644 --- a/src/com/android/dialer/dialpad/SmartDialLoaderTask.java +++ b/src/com/android/dialer/dialpad/SmartDialLoaderTask.java @@ -93,6 +93,8 @@ public class SmartDialLoaderTask extends AsyncTask<String, Integer, List<SmartDi private ArrayList<SmartDialEntry> getContactMatches() { final SmartDialTrie trie = mContactsCache.getContacts(); + final boolean matchNanp = mContactsCache.getUserInNanpRegion(); + if (DEBUG) { Log.d(LOG_TAG, "Size of cache: " + trie.size()); } @@ -119,12 +121,14 @@ public class SmartDialLoaderTask extends AsyncTask<String, Integer, List<SmartDi } duplicates.add(contactMatch); final boolean matches = mNameMatcher.matches(contact.displayName); + candidates.add(new SmartDialEntry( contact.displayName, Contacts.getLookupUri(contact.id, contact.lookupKey), contact.phoneNumber, mNameMatcher.getMatchPositions(), - mNameMatcher.matchesNumber(contact.phoneNumber, mNameMatcher.getQuery()) + SmartDialNameMatcher.matchesNumber(contact.phoneNumber, + mNameMatcher.getQuery(), matchNanp) )); if (candidates.size() >= MAX_ENTRIES) { break; diff --git a/src/com/android/dialer/dialpad/SmartDialNameMatcher.java b/src/com/android/dialer/dialpad/SmartDialNameMatcher.java index f805abff1..4d3948100 100644 --- a/src/com/android/dialer/dialpad/SmartDialNameMatcher.java +++ b/src/com/android/dialer/dialpad/SmartDialNameMatcher.java @@ -437,20 +437,54 @@ public class SmartDialNameMatcher { } /** + * Matches a phone number against a query, taking care of formatting characters and also + * taking into account country code prefixes and special NANP number treatment. + * + * @param phoneNumber - Raw phone number + * @param query - Normalized query (only contains numbers from 0-9) + * @param matchNanp - Whether or not to do special matching for NANP numbers + * @return {@literal null} if the number and the query don't match, a valid + * SmartDialMatchPosition with the matching positions otherwise + */ + public static SmartDialMatchPosition matchesNumber(String phoneNumber, String query, + boolean matchNanp) { + // Try matching the number as is + SmartDialMatchPosition matchPos = matchesNumberWithOffset(phoneNumber, query, 0); + if (matchPos == null) { + // Try matching the number without the '+' prefix, if any + int offset = SmartDialTrie.getOffsetWithoutCountryCode(phoneNumber); + if (offset > 0) { + matchPos = matchesNumberWithOffset(phoneNumber, query, offset); + } else if (matchNanp) { + // Try matching NANP numbers + final int[] offsets = SmartDialTrie.getOffsetForNANPNumbers(phoneNumber); + for (int i = 0; i < offsets.length; i++) { + matchPos = matchesNumberWithOffset(phoneNumber, query, offsets[i]); + if (matchPos != null) break; + } + } + } + return matchPos; + } + + /** * Matches a phone number against a query, taking care of formatting characters * * @param phoneNumber - Raw phone number * @param query - Normalized query (only contains numbers from 0-9) + * @param offset - The position in the number to start the match against (used to ignore + * leading prefixes/country codes) * @return {@literal null} if the number and the query don't match, a valid * SmartDialMatchPosition with the matching positions otherwise */ - public SmartDialMatchPosition matchesNumber(String phoneNumber, String query) { + private static SmartDialMatchPosition matchesNumberWithOffset(String phoneNumber, String query, + int offset) { if (TextUtils.isEmpty(phoneNumber) || TextUtils.isEmpty(query)) { return null; } int queryAt = 0; - int numberAt = 0; - for (int i = 0; i < phoneNumber.length(); i++) { + int numberAt = offset; + for (int i = offset; i < phoneNumber.length(); i++) { if (queryAt == query.length()) { break; } @@ -460,13 +494,25 @@ public class SmartDialNameMatcher { return null; } queryAt++; - numberAt++; } else { - numberAt++; - continue; + if (queryAt == 0) { + // Found a separator before any part of the query was matched, so advance the + // offset to avoid prematurely highlighting separators before the rest of the + // query. + // E.g. don't highlight the first '-' if we're matching 1-510-111-1111 with + // '510'. + // However, if the current offset is 0, just include the beginning separators + // anyway, otherwise the highlighting ends up looking weird. + // E.g. if we're matching (510)-111-1111 with '510', we should include the + // first '('. + if (offset != 0) { + offset++; + } + } } + numberAt++; } - return new SmartDialMatchPosition(0, numberAt); + return new SmartDialMatchPosition(0 + offset, numberAt); } /** diff --git a/src/com/android/dialer/dialpad/SmartDialTrie.java b/src/com/android/dialer/dialpad/SmartDialTrie.java index 981926717..9dfc0c95a 100644 --- a/src/com/android/dialer/dialpad/SmartDialTrie.java +++ b/src/com/android/dialer/dialpad/SmartDialTrie.java @@ -20,27 +20,58 @@ import android.text.TextUtils; import com.android.dialer.dialpad.SmartDialCache.ContactNumber; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; /** * Prefix trie where the only allowed characters are the characters '0' to '9'. Multiple contacts - * can occupy the same nodes. Provides functions to get all contacts that lie on or below a node. - * This is useful for retrieving all contacts that start with that prefix. + * can occupy the same nodes. + * + * <p>Provides functions to get all contacts that lie on or below a node. + * This is useful for retrieving all contacts that start with that prefix.</p> + * + * <p>Also contains special logic to handle NANP numbers in the case that the user is from a region + * that uses NANP numbers.</p> */ public class SmartDialTrie { final Node mRoot = new Node(); private int mSize = 0; private final char[] mCharacterMap; + private final boolean mFormatNanp; + + // Static set of all possible country codes in the world + public static Set<String> sCountryCodes = null; public SmartDialTrie() { // Use the latin letter to digit map by default if none provided - this(SmartDialNameMatcher.LATIN_LETTERS_TO_DIGITS); + this(SmartDialNameMatcher.LATIN_LETTERS_TO_DIGITS, false); + } + + /** + * Creates a new SmartDialTrie. + * + * @param formatNanp True if inserted numbers are to be treated as NANP numbers + * such that numbers are automatically broken up by country prefix and area code. + */ + @VisibleForTesting + public SmartDialTrie(boolean formatNanp) { + this(SmartDialNameMatcher.LATIN_LETTERS_TO_DIGITS, formatNanp); } - public SmartDialTrie(char[] charMap) { + /** + * Creates a new SmartDialTrie. + * + * @param charMap Mapping of characters to digits to use when inserting names into the trie. + * @param formatNanp True if inserted numbers are to be treated as NANP numbers + * such that numbers are automatically broken up by country prefix and area code. + */ + public SmartDialTrie(char[] charMap, boolean formatNanp) { mCharacterMap = charMap; + mFormatNanp = formatNanp; } /** @@ -90,10 +121,100 @@ public class SmartDialTrie { putForPrefix(contact, mRoot, toByteArray(contact.displayName), 0, contact.displayName.length(), true, true); // We don't need to do the same for phone numbers since we only make one pass over them. - putNumber(contact, contact.phoneNumber); + // Strip the calling code from the phone number here + if (!TextUtils.isEmpty(contact.phoneNumber)) { + // Handle country codes for numbers with a + prefix + final int offset = getOffsetWithoutCountryCode(contact.phoneNumber); + if (offset > 0) { + putNumber(contact, contact.phoneNumber, offset); + } else if (mFormatNanp) { + // Special case handling for NANP numbers (1-xxx-xxx-xxxx) + final String stripped = SmartDialNameMatcher.normalizeNumber(contact.phoneNumber); + if (!TextUtils.isEmpty(stripped)) { + int trunkPrefixOffset = 0; + if (stripped.charAt(0) == '1') { + // If the number starts with 1, we can assume its the trunk prefix. + trunkPrefixOffset = 1; + } + if (stripped.length() == (10 + trunkPrefixOffset)) { + // Valid NANP number + if (trunkPrefixOffset != 0) { + // Add the digits that follow the 1st digit (trunk prefix) + // If trunkPrefixOffset is 0, we will add the number as is anyway, so + // don't bother. + putNumber(contact, stripped, trunkPrefixOffset); + } + // Add the digits that follow the next 3 digits (area code) + putNumber(contact, stripped, 3 + trunkPrefixOffset); + } + } + } + putNumber(contact, contact.phoneNumber); + } mSize++; } + public static int getOffsetWithoutCountryCode(String number) { + if (!TextUtils.isEmpty(number)) { + if (number.charAt(0) == '+') { + // check for international code here + for (int i = 1; i <= 1 + 3; i++) { + if (number.length() <= i) break; + if (isValidCountryCode(number.substring(1, i))) { + return i; + } + } + } + } + return -1; + } + + /** + * Used by SmartDialNameMatcher to determine which character in the phone number to start + * the matching process from for a NANP formatted number. + * + * @param number Raw phone number + * @return An empty array if the provided number does not appear to be an NANP number, + * and an array containing integer offsets for the number (starting after the '1' prefix, + * and the area code prefix respectively. + */ + public static int[] getOffsetForNANPNumbers(String number) { + int validDigits = 0; + boolean hasPrefix = false; + int firstOffset = 0; // Tracks the location of the first digit after the '1' prefix + int secondOffset = 0; // Tracks the location of the first digit after the area code + for (int i = 0; i < number.length(); i++) { + final char ch = number.charAt(i); + if (ch >= '0' && ch <= '9') { + if (validDigits == 0) { + // Check the first digit to see if it is 1 + if (ch == '1') { + if (hasPrefix) { + // Prefix has two '1's in a row. Invalid number, since area codes + // cannot start with 1, so just bail + break; + } + hasPrefix = true; + continue; + } + } + validDigits++; + if (validDigits == 1) { + // Found the first digit after the country code + firstOffset = i; + } else if (validDigits == 4) { + // Found the first digit after the area code + secondOffset = i; + } + } + + } + if (validDigits == 10) { + return hasPrefix ? new int[] {firstOffset, secondOffset} : new int[] {secondOffset}; + } + return new int[0]; + } + @VisibleForTesting /* package */ byte[] toByteArray(CharSequence chars) { final int length = chars.length(); @@ -115,12 +236,24 @@ public class SmartDialTrie { * * @param contact - Contact to add to the trie * @param phoneNumber - Phone number of the contact + */ + private void putNumber(ContactNumber contact, String phoneNumber) { + putNumber(contact, phoneNumber, 0); + } + + /** + * Puts a phone number and its associated contact into the prefix trie. + * + * @param contact - Contact to add to the trie + * @param phoneNumber - Phone number of the contact + * @param offset - The nth character of the phone number to start from */ - public void putNumber(ContactNumber contact, String phoneNumber) { + private void putNumber(ContactNumber contact, String phoneNumber, int offSet) { + Preconditions.checkArgument(offSet < phoneNumber.length()); Node current = mRoot; final int length = phoneNumber.length(); char ch; - for (int i = 0; i < length; i++) { + for (int i = offSet; i < length; i++) { ch = phoneNumber.charAt(i); if (ch >= '0' && ch <= '9') { current = current.getChild(ch, true); @@ -183,6 +316,7 @@ public class SmartDialTrie { current.add(contact); } + @VisibleForTesting public int size() { return mSize; } @@ -247,4 +381,231 @@ public class SmartDialTrie { return mContents; } } + + private static boolean isValidCountryCode(String countryCode) { + if (sCountryCodes == null) { + sCountryCodes = initCountryCodes(); + } + return sCountryCodes.contains(countryCode); + } + + private static Set<String> initCountryCodes() { + final HashSet<String> result = new HashSet<String>(); + result.add("1"); + result.add("7"); + result.add("20"); + result.add("27"); + result.add("30"); + result.add("31"); + result.add("32"); + result.add("33"); + result.add("34"); + result.add("36"); + result.add("39"); + result.add("40"); + result.add("41"); + result.add("43"); + result.add("44"); + result.add("45"); + result.add("46"); + result.add("47"); + result.add("48"); + result.add("49"); + result.add("51"); + result.add("52"); + result.add("53"); + result.add("54"); + result.add("55"); + result.add("56"); + result.add("57"); + result.add("58"); + result.add("60"); + result.add("61"); + result.add("62"); + result.add("63"); + result.add("64"); + result.add("65"); + result.add("66"); + result.add("81"); + result.add("82"); + result.add("84"); + result.add("86"); + result.add("90"); + result.add("91"); + result.add("92"); + result.add("93"); + result.add("94"); + result.add("95"); + result.add("98"); + result.add("211"); + result.add("212"); + result.add("213"); + result.add("216"); + result.add("218"); + result.add("220"); + result.add("221"); + result.add("222"); + result.add("223"); + result.add("224"); + result.add("225"); + result.add("226"); + result.add("227"); + result.add("228"); + result.add("229"); + result.add("230"); + result.add("231"); + result.add("232"); + result.add("233"); + result.add("234"); + result.add("235"); + result.add("236"); + result.add("237"); + result.add("238"); + result.add("239"); + result.add("240"); + result.add("241"); + result.add("242"); + result.add("243"); + result.add("244"); + result.add("245"); + result.add("246"); + result.add("247"); + result.add("248"); + result.add("249"); + result.add("250"); + result.add("251"); + result.add("252"); + result.add("253"); + result.add("254"); + result.add("255"); + result.add("256"); + result.add("257"); + result.add("258"); + result.add("260"); + result.add("261"); + result.add("262"); + result.add("263"); + result.add("264"); + result.add("265"); + result.add("266"); + result.add("267"); + result.add("268"); + result.add("269"); + result.add("290"); + result.add("291"); + result.add("297"); + result.add("298"); + result.add("299"); + result.add("350"); + result.add("351"); + result.add("352"); + result.add("353"); + result.add("354"); + result.add("355"); + result.add("356"); + result.add("357"); + result.add("358"); + result.add("359"); + result.add("370"); + result.add("371"); + result.add("372"); + result.add("373"); + result.add("374"); + result.add("375"); + result.add("376"); + result.add("377"); + result.add("378"); + result.add("379"); + result.add("380"); + result.add("381"); + result.add("382"); + result.add("385"); + result.add("386"); + result.add("387"); + result.add("389"); + result.add("420"); + result.add("421"); + result.add("423"); + result.add("500"); + result.add("501"); + result.add("502"); + result.add("503"); + result.add("504"); + result.add("505"); + result.add("506"); + result.add("507"); + result.add("508"); + result.add("509"); + result.add("590"); + result.add("591"); + result.add("592"); + result.add("593"); + result.add("594"); + result.add("595"); + result.add("596"); + result.add("597"); + result.add("598"); + result.add("599"); + result.add("670"); + result.add("672"); + result.add("673"); + result.add("674"); + result.add("675"); + result.add("676"); + result.add("677"); + result.add("678"); + result.add("679"); + result.add("680"); + result.add("681"); + result.add("682"); + result.add("683"); + result.add("685"); + result.add("686"); + result.add("687"); + result.add("688"); + result.add("689"); + result.add("690"); + result.add("691"); + result.add("692"); + result.add("800"); + result.add("808"); + result.add("850"); + result.add("852"); + result.add("853"); + result.add("855"); + result.add("856"); + result.add("870"); + result.add("878"); + result.add("880"); + result.add("881"); + result.add("882"); + result.add("883"); + result.add("886"); + result.add("888"); + result.add("960"); + result.add("961"); + result.add("962"); + result.add("963"); + result.add("964"); + result.add("965"); + result.add("966"); + result.add("967"); + result.add("968"); + result.add("970"); + result.add("971"); + result.add("972"); + result.add("973"); + result.add("974"); + result.add("975"); + result.add("976"); + result.add("977"); + result.add("979"); + result.add("992"); + result.add("993"); + result.add("994"); + result.add("995"); + result.add("996"); + result.add("998"); + return result; + } } |