Allow smart dialing to ignore country code/area code

Add handling of country codes if the number starts with a '+' prefix
Also add special case handling for NANP numbers in SmartDialTrie
if we determine that the user is in North America.
Save user's SIM country ISO into SharedPreferences.
Add SmartDialNameMatcher methods to correctly match NANP numbers
Fix tests to use assertTrue/assertFalse instead of assertEquals

Bug 8292294
Change-Id: Ic61dac75ee3b772986d3008240bbda81322c8f31
diff --git a/src/com/android/dialer/dialpad/SmartDialCache.java b/src/com/android/dialer/dialpad/SmartDialCache.java
index 79cc727..7a11b15 100644
--- a/src/com/android/dialer/dialpad/SmartDialCache.java
+++ b/src/com/android/dialer/dialpad/SmartDialCache.java
@@ -19,22 +19,27 @@
 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 @@
                 + 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 @@
         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 @@
                 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 @@
             // 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 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 c5fc657..08c5265 100644
--- a/src/com/android/dialer/dialpad/SmartDialLoaderTask.java
+++ b/src/com/android/dialer/dialpad/SmartDialLoaderTask.java
@@ -93,6 +93,8 @@
     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 @@
             }
             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 f805abf..4d39481 100644
--- a/src/com/android/dialer/dialpad/SmartDialNameMatcher.java
+++ b/src/com/android/dialer/dialpad/SmartDialNameMatcher.java
@@ -437,20 +437,54 @@
     }
 
     /**
+     * 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 @@
                     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 9819267..9dfc0c9 100644
--- a/src/com/android/dialer/dialpad/SmartDialTrie.java
+++ b/src/com/android/dialer/dialpad/SmartDialTrie.java
@@ -20,27 +20,58 @@
 
 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);
     }
 
-    public SmartDialTrie(char[] charMap) {
+    /**
+     * 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);
+    }
+
+    /**
+     * 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 @@
         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 @@
      *
      * @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 @@
         current.add(contact);
     }
 
+    @VisibleForTesting
     public int size() {
         return mSize;
     }
@@ -247,4 +381,231 @@
             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;
+    }
 }
diff --git a/tests/src/com/android/dialer/dialpad/SmartDialCacheTest.java b/tests/src/com/android/dialer/dialpad/SmartDialCacheTest.java
new file mode 100644
index 0000000..8d96eda
--- /dev/null
+++ b/tests/src/com/android/dialer/dialpad/SmartDialCacheTest.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.dialpad;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.TestCase;
+
+@SmallTest
+public class SmartDialCacheTest extends TestCase {
+    public void testIsCountryNanp_CaseInsensitive() {
+        assertFalse(SmartDialCache.isCountryNanp(null));
+        assertFalse(SmartDialCache.isCountryNanp("CN"));
+        assertFalse(SmartDialCache.isCountryNanp("HK"));
+        assertFalse(SmartDialCache.isCountryNanp("uk"));
+        assertFalse(SmartDialCache.isCountryNanp("sg"));
+        assertTrue(SmartDialCache.isCountryNanp("US"));
+        assertTrue(SmartDialCache.isCountryNanp("CA"));
+        assertTrue(SmartDialCache.isCountryNanp("AS"));
+        assertTrue(SmartDialCache.isCountryNanp("AI"));
+        assertTrue(SmartDialCache.isCountryNanp("AG"));
+        assertTrue(SmartDialCache.isCountryNanp("BS"));
+        assertTrue(SmartDialCache.isCountryNanp("BB"));
+        assertTrue(SmartDialCache.isCountryNanp("bm"));
+        assertTrue(SmartDialCache.isCountryNanp("vg"));
+        assertTrue(SmartDialCache.isCountryNanp("ky"));
+        assertTrue(SmartDialCache.isCountryNanp("dm"));
+        assertTrue(SmartDialCache.isCountryNanp("do"));
+        assertTrue(SmartDialCache.isCountryNanp("gd"));
+        assertTrue(SmartDialCache.isCountryNanp("gu"));
+        assertTrue(SmartDialCache.isCountryNanp("jm"));
+        assertTrue(SmartDialCache.isCountryNanp("pr"));
+        assertTrue(SmartDialCache.isCountryNanp("ms"));
+        assertTrue(SmartDialCache.isCountryNanp("mp"));
+        assertTrue(SmartDialCache.isCountryNanp("kn"));
+        assertTrue(SmartDialCache.isCountryNanp("lc"));
+        assertTrue(SmartDialCache.isCountryNanp("vc"));
+        assertTrue(SmartDialCache.isCountryNanp("tt"));
+        assertTrue(SmartDialCache.isCountryNanp("tc"));
+        assertTrue(SmartDialCache.isCountryNanp("vi"));
+    }
+}
diff --git a/tests/src/com/android/dialer/dialpad/SmartDialNameMatcherTest.java b/tests/src/com/android/dialer/dialpad/SmartDialNameMatcherTest.java
index 08939b4..3c481d0 100644
--- a/tests/src/com/android/dialer/dialpad/SmartDialNameMatcherTest.java
+++ b/tests/src/com/android/dialer/dialpad/SmartDialNameMatcherTest.java
@@ -91,6 +91,8 @@
         checkMatches("William John-Smith", "5764", true, 8, 9, 13, 16);
         // make sure multiple spaces don't mess things up
         checkMatches("William        John---Smith", "5764", true, 15, 16, 22, 25);
+
+        checkMatches("Berkeley Hair-Studio", "788346", true, 14, 20);
     }
 
     public void testMatches_repeatedSeparators() {
@@ -114,6 +116,7 @@
     public void testMatches_umlaut() {
         checkMatches("ÄÖÜäöü", "268268", true, 0, 6);
     }
+
     // TODO: Great if it was treated as "s" or "ss. Figure out if possible without prefix trie?
     @Suppress
     public void testMatches_germanSharpS() {
@@ -135,6 +138,80 @@
         fail("Cyrillic letters aren't supported yet.");
     }
 
+
+    public void testMatches_NumberBasic() {
+        // Simple basic examples that start the match from the start of the number
+        checkMatchesNumber("5103337596", "510", true, 0, 3);
+        checkMatchesNumber("5103337596", "511", false, 0, 0);
+        checkMatchesNumber("5103337596", "5103337596", true, 0, 10);
+        checkMatchesNumber("123-456-789", "123456789", true, 0, 11);
+        checkMatchesNumber("123-456-789", "123456788", false, 0, 0);
+        checkMatchesNumber("09999999999", "099", true, 0, 3);
+    }
+
+    public void testMatches_NumberWithCountryCode() {
+        // These matches should ignore the country prefix
+        // USA (+1)
+        checkMatchesNumber("+15103337596", "5103337596", true, 2, 12);
+        checkMatchesNumber("+15103337596", "15103337596", true, 0, 12);
+
+        // Singapore (+65)
+        checkMatchesNumber("+6591776930", "6591", true, 0, 5);
+        checkMatchesNumber("+6591776930", "9177", true, 3, 7);
+        checkMatchesNumber("+6591776930", "5917", false, 3, 7);
+
+        // Hungary (+36)
+        checkMatchesNumber("+3612345678", "361234", true, 0, 7);
+        checkMatchesNumber("+3612345678", "1234", true, 3, 7);
+
+        // Hongkong (+852)
+        checkMatchesNumber("+852 2222 2222", "85222222222", true, 0, 14);
+        checkMatchesNumber("+852 2222 3333", "2222", true, 5, 9);
+
+        // Invalid (+854)
+        checkMatchesNumber("+854 1111 2222", "8541111", true, 0, 9);
+        checkMatchesNumber("+854 1111 2222", "1111", false, 0, 0);
+    }
+
+    public void testMatches_NumberNANP() {
+        // An 11 digit number prefixed with 1 should be matched by the 10 digit number, as well as
+        // the 7 digit number (without area code)
+        checkMatchesNumber("1-510-333-7596", "5103337596", true, true, 2, 14);
+        checkMatchesNumber("1-510-333-7596", "3337596", true, true, 6, 14);
+
+        // Invalid NANP numbers should not be matched
+        checkMatchesNumber("1-510-333-759", "510333759", false, true, 0, 0);
+        checkMatchesNumber("510-333-759", "333759", false, true, 0, 0);
+
+        // match should fail if NANP flag is switched off
+        checkMatchesNumber("1-510-333-7596", "3337596", false, false, 0, 0);
+
+        // A 10 digit number without a 1 prefix should be matched by the 7 digit number
+        checkMatchesNumber("(650) 292 2323", "2922323", true, true, 6, 14);
+        checkMatchesNumber("(650) 292 2323", "6502922323", true, true, 0, 14);
+        // match should fail if NANP flag is switched off
+        checkMatchesNumber("(650) 292 2323", "2922323", false, false, 0, 0);
+        // But this should still match (since it is the full number)
+        checkMatchesNumber("(650) 292 2323", "6502922323", true, false, 0, 14);
+    }
+
+
+    private void checkMatchesNumber(String number, String query, boolean expectedMatches,
+            int matchStart, int matchEnd) {
+        checkMatchesNumber(number, query, expectedMatches, false, matchStart, matchEnd);
+    }
+
+    private void checkMatchesNumber(String number, String query, boolean expectedMatches,
+            boolean matchNanp, int matchStart, int matchEnd) {
+        final SmartDialMatchPosition pos = SmartDialNameMatcher.matchesNumber(number, query,
+                matchNanp);
+        assertEquals(expectedMatches, pos != null);
+        if (expectedMatches) {
+            assertEquals("start", matchStart, pos.start);
+            assertEquals("end", matchEnd, pos.end);
+        }
+    }
+
     private void checkMatches(String displayName, String query, boolean expectedMatches,
             int... expectedMatchPositions) {
         final SmartDialNameMatcher matcher = new SmartDialNameMatcher(query);
diff --git a/tests/src/com/android/dialer/dialpad/SmartDialTrieTest.java b/tests/src/com/android/dialer/dialpad/SmartDialTrieTest.java
index 0378555..7f55263 100644
--- a/tests/src/com/android/dialer/dialpad/SmartDialTrieTest.java
+++ b/tests/src/com/android/dialer/dialpad/SmartDialTrieTest.java
@@ -48,11 +48,11 @@
         final ContactNumber jasonsmitt = new ContactNumber(1, "Jason Smitt", "0", "1", 2);
         trie.put(jasonsmith);
         trie.put(jasonsmitt);
-        assertEquals(true, trie.getAllWithPrefix("5276676484").contains(jasonsmith));
-        assertEquals(false, trie.getAllWithPrefix("5276676484").contains(jasonsmitt));
+        assertTrue(trie.getAllWithPrefix("5276676484").contains(jasonsmith));
+        assertFalse(trie.getAllWithPrefix("5276676484").contains(jasonsmitt));
 
-        assertEquals(false, trie.getAllWithPrefix("5276676488").contains(jasonsmith));
-        assertEquals(true, trie.getAllWithPrefix("5276676488").contains(jasonsmitt));
+        assertFalse(trie.getAllWithPrefix("5276676488").contains(jasonsmith));
+        assertTrue(trie.getAllWithPrefix("5276676488").contains(jasonsmitt));
 
     }
 
@@ -66,18 +66,18 @@
         trie.put(jasonsmitt);
 
         // 6279 corresponds to mary = "Mary Jane" but not "Jason Smitt"
-        assertEquals(true, checkContains(trie, maryjane, "6279"));
-        assertEquals(false, checkContains(trie, jasonsmitt, "6279"));
+        assertTrue(checkContains(trie, maryjane, "6279"));
+        assertFalse(checkContains(trie, jasonsmitt, "6279"));
 
         // 72 corresponds to sa = "Sarah Smith" but not "Jason Smitt" or "Mary Jane"
-        assertEquals(false, checkContains(trie, maryjane, "72"));
-        assertEquals(true, checkContains(trie, sarahsmith, "72"));
-        assertEquals(false, checkContains(trie, jasonsmitt, "72"));
+        assertFalse(checkContains(trie, maryjane, "72"));
+        assertTrue(checkContains(trie, sarahsmith, "72"));
+        assertFalse(checkContains(trie, jasonsmitt, "72"));
 
         // 76 corresponds to sm = "Sarah Smith" and "Jason Smitt" but not "Mary Jane"
-        assertEquals(false, checkContains(trie, maryjane, "76"));
-        assertEquals(true, checkContains(trie, sarahsmith, "76"));
-        assertEquals(true, checkContains(trie, jasonsmitt, "76"));
+        assertFalse(checkContains(trie, maryjane, "76"));
+        assertTrue(checkContains(trie, sarahsmith, "76"));
+        assertTrue(checkContains(trie, jasonsmitt, "76"));
     }
 
     public void testPutForNameTokens() {
@@ -86,11 +86,11 @@
         trie.put(jasonfwilliams);
 
         // 527 corresponds to jas = "Jason"
-        assertEquals(true, checkContains(trie, jasonfwilliams, "527"));
+        assertTrue(checkContains(trie, jasonfwilliams, "527"));
         // 945 corresponds to wil = "Wil"
-        assertEquals(true, checkContains(trie, jasonfwilliams, "945"));
+        assertTrue(checkContains(trie, jasonfwilliams, "945"));
         // 66 doesn't match
-        assertEquals(false, checkContains(trie, jasonfwilliams, "66"));
+        assertFalse(checkContains(trie, jasonfwilliams, "66"));
     }
 
     public void testPutForInitialMatches() {
@@ -99,21 +99,21 @@
                 new ContactNumber(0, "Martin Jr Harry", "0", "0", 1);
         trie.put(martinjuniorharry);
         // 654 corresponds to mjh = "(M)artin (J)r (H)arry"
-        assertEquals(true, checkContains(trie, martinjuniorharry, "654"));
+        assertTrue(checkContains(trie, martinjuniorharry, "654"));
         // The reverse (456) does not match (for now)
-        assertEquals(false, checkContains(trie, martinjuniorharry, "456"));
+        assertFalse(checkContains(trie, martinjuniorharry, "456"));
         // 6542 corresponds to mjha = "(M)artin (J)r (Ha)rry"
-        assertEquals(true, checkContains(trie, martinjuniorharry, "6542"));
+        assertTrue(checkContains(trie, martinjuniorharry, "6542"));
         // 542 corresponds to jha = "Martin (J)r (Ha)rry"
-        assertEquals(true, checkContains(trie, martinjuniorharry, "542"));
+        assertTrue(checkContains(trie, martinjuniorharry, "542"));
         // 547 doesn't match
-        assertEquals(false, checkContains(trie, martinjuniorharry, "547"));
+        assertFalse(checkContains(trie, martinjuniorharry, "547"));
         // 655 doesn't match
-        assertEquals(false, checkContains(trie, martinjuniorharry, "655"));
+        assertFalse(checkContains(trie, martinjuniorharry, "655"));
         // 653 doesn't match
-        assertEquals(false, checkContains(trie, martinjuniorharry, "653"));
+        assertFalse(checkContains(trie, martinjuniorharry, "653"));
         // 6543 doesn't match
-        assertEquals(false, checkContains(trie, martinjuniorharry, "6543"));
+        assertFalse(checkContains(trie, martinjuniorharry, "6543"));
     }
 
     public void testSeparators() {
@@ -124,9 +124,9 @@
         for (int i = 0; i < name.length(); i++) {
             // separators at position 8 and 12
             if (i == 8 || i == 14) {
-                assertEquals(true, bytes[i] == -1);
+                assertTrue(bytes[i] == -1);
             } else {
-                assertEquals(false, bytes[i] == -1);
+                assertFalse(bytes[i] == -1);
             }
         }
     }
@@ -137,8 +137,8 @@
         final ContactNumber bronte = new ContactNumber(2, "Brontë", "0", "1", 2);
         trie.put(reenee);
         trie.put(bronte);
-        assertEquals(true, checkContains(trie, reenee, "733633"));
-        assertEquals(true, checkContains(trie, bronte, "276683"));
+        assertTrue(checkContains(trie, reenee, "733633"));
+        assertTrue(checkContains(trie, bronte, "276683"));
     }
 
     public void testPutForNumbers() {
@@ -150,15 +150,111 @@
         final ContactNumber contactno3 = new ContactNumber(0, "James", "+13684976334", "0", 1);
         trie.put(contactno3);
         // all phone numbers belonging to the contact should correspond to it
-        assertEquals(true, checkContains(trie, contactno1, "510"));
-        assertEquals(false, checkContains(trie, contactno1, "511"));
-        assertEquals(true, checkContains(trie, contactno2, "77212862357"));
-        assertEquals(false, checkContains(trie, contactno2, "77212862356"));
-        assertEquals(true, checkContains(trie, contactno3, "1368"));
-        assertEquals(false, checkContains(trie, contactno3, "1367"));
+        assertTrue(checkContains(trie, contactno1, "510"));
+        assertFalse(checkContains(trie, contactno1, "511"));
+        assertTrue(checkContains(trie, contactno2, "77212862357"));
+        assertFalse(checkContains(trie, contactno2, "77212862356"));
+        assertTrue(checkContains(trie, contactno3, "1368"));
+        assertFalse(checkContains(trie, contactno3, "1367"));
 
     }
 
+    public void testPutNumbersCountryCode() {
+        final SmartDialTrie trie = new SmartDialTrie();
+        final ContactNumber contactno1 = new ContactNumber(0, "James", "+13684976334", "0", 1);
+        trie.put(contactno1);
+
+        // all phone numbers belonging to the contact should correspond to it
+        assertTrue(checkContains(trie, contactno1, "1368"));
+        assertTrue(checkContains(trie, contactno1, "368497"));
+        assertFalse(checkContains(trie, contactno1, "2368497"));
+
+        final ContactNumber contactno2 = new ContactNumber(0, "Jason", "+65 9177-6930", "0", 1);
+        trie.put(contactno2);
+
+        assertTrue(checkContains(trie, contactno2, "6591776930"));
+        assertTrue(checkContains(trie, contactno2, "91776930"));
+        assertFalse(checkContains(trie, contactno2, "591776930"));
+
+        final ContactNumber contactno3 = new ContactNumber(0, "Mike", "+85212345678", "0", 1);
+        trie.put(contactno3);
+        assertTrue(checkContains(trie, contactno3, "85212345678"));
+        assertTrue(checkContains(trie, contactno3, "12345678"));
+        assertFalse(checkContains(trie, contactno2, "5212345678"));
+
+        // Invalid country code, don't try to parse it
+        final ContactNumber contactno4 = new ContactNumber(0, "Invalid", "+85112345678", "0", 1);
+        trie.put(contactno4);
+        assertTrue(checkContains(trie, contactno4, "85112345678"));
+        assertFalse(checkContains(trie, contactno4, "12345678"));
+
+        final ContactNumber contactno5 = new ContactNumber(0, "Invalid", "+852", "0", 1);
+        // Shouldn't crash
+        trie.put(contactno5);
+    }
+
+    // Tests special case handling for NANP numbers
+    public void testPutNumbersNANP() {
+
+        final SmartDialTrie trie = new SmartDialTrie(true /* formatNanp */);
+        // Unformatted number with 1 prefix
+        final ContactNumber contactno1 = new ContactNumber(0, "James", "16503337596", "0", 1);
+        trie.put(contactno1);
+
+        assertTrue(checkContains(trie, contactno1, "16503337596"));
+        assertTrue(checkContains(trie, contactno1, "6503337596"));
+        assertTrue(checkContains(trie, contactno1, "3337596"));
+
+        // Number with seperators
+        final ContactNumber contactno2 = new ContactNumber(0, "Michael", "5109921234", "0", 1);
+        trie.put(contactno2);
+        assertTrue(checkContains(trie, contactno2, "5109921234"));
+        assertTrue(checkContains(trie, contactno2, "9921234"));
+
+        // Number with area code only + separators
+        final ContactNumber contactno3 = new ContactNumber(0, "Jason", "(415)-123-4567", "0", 1);
+        trie.put(contactno3);
+        assertTrue(checkContains(trie, contactno3, "4151234567"));
+        assertTrue(checkContains(trie, contactno3, "1234567"));
+
+        // Number without +1 prefix but is a NANP number
+        final ContactNumber contactno4 = new ContactNumber(0, "Mike", "1 510-284-9170", "0", 1);
+        trie.put(contactno4);
+        assertTrue(checkContains(trie, contactno4, "15102849170"));
+        assertTrue(checkContains(trie, contactno4, "5102849170"));
+        assertTrue(checkContains(trie, contactno4, "2849170"));
+
+        // Invalid number(has 1 prefix, but is only 10 characters long)
+        final ContactNumber contactno5 = new ContactNumber(0, "Invalid", "1-415-123-123", "0", 1);
+        trie.put(contactno5);
+        // It should still be inserted as is
+        assertTrue(checkContains(trie, contactno5, "1415123123"));
+        // But the NANP special case handling should not work
+        assertFalse(checkContains(trie, contactno5, "415123123"));
+        assertFalse(checkContains(trie, contactno5, "123123"));
+
+        // Invalid number(only 9 characters long)
+        final ContactNumber contactno6 = new ContactNumber(0, "Invalid2", "415-123-123", "0", 1);
+        trie.put(contactno6);
+        // It should still be inserted as is
+        assertTrue(checkContains(trie, contactno6, "415123123"));
+        // But the NANP special case handling should not work
+        assertFalse(checkContains(trie, contactno6, "123123"));
+
+        // If user's region is determined to be not in North America, then the NANP number
+        // workarounds should not be applied
+        final SmartDialTrie trieNonNANP = new SmartDialTrie();
+
+        trieNonNANP.put(contactno3);
+        assertTrue(checkContains(trieNonNANP, contactno3, "4151234567"));
+        assertFalse(checkContains(trieNonNANP, contactno3, "1234567"));
+
+        trieNonNANP.put(contactno4);
+        assertTrue(checkContains(trieNonNANP, contactno4, "15102849170"));
+        assertFalse(checkContains(trieNonNANP, contactno4, "5102849170"));
+        assertFalse(checkContains(trieNonNANP, contactno4, "2849170"));
+    }
+
     public void testNodeConstructor() {
         final Node n = new Node();
         // Node member variables should not be initialized by default at construction to reduce
@@ -185,8 +281,8 @@
         final ContactNumber contact = new ContactNumber(0, "James", "510-527-2357", "0", 1);
         final ContactNumber contactNotIn = new ContactNumber(2, "Jason Smitt", "0", "2", 3);
         n.add(contact);
-        assertEquals(true, n.getContents().contains(contact));
-        assertEquals(false, n.getContents().contains(contactNotIn));
+        assertTrue(n.getContents().contains(contact));
+        assertFalse(n.getContents().contains(contactNotIn));
     }
 
     private boolean checkContains(SmartDialTrie trie, ContactNumber contact, CharSequence prefix) {