Home | History | Annotate | Download | only in dialpad
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.dialer.dialpad;
     18 
     19 import android.content.Context;
     20 
     21 import android.content.SharedPreferences;
     22 import android.preference.PreferenceManager;
     23 import android.telephony.TelephonyManager;
     24 import android.text.TextUtils;
     25 
     26 import com.google.common.annotations.VisibleForTesting;
     27 import com.google.common.collect.Lists;
     28 
     29 import java.util.ArrayList;
     30 import java.util.HashSet;
     31 import java.util.Set;
     32 
     33 /**
     34  * Smart Dial utility class to find prefixes of contacts. It contains both methods to find supported
     35  * prefix combinations for contact names, and also methods to find supported prefix combinations for
     36  * contacts' phone numbers. Each contact name is separated into several tokens, such as first name,
     37  * middle name, family name etc. Each phone number is also separated into country code, NANP area
     38  * code, and local number if such separation is possible.
     39  */
     40 public class SmartDialPrefix {
     41 
     42     /** The number of starting and ending tokens in a contact's name considered for initials.
     43      * For example, if both constants are set to 2, and a contact's name is
     44      * "Albert Ben Charles Daniel Ed Foster", the first two tokens "Albert" "Ben", and last two
     45      * tokens "Ed" "Foster" can be replaced by their initials in contact name matching.
     46      * Users can look up this contact by combinations of his initials such as "AF" "BF" "EF" "ABF"
     47      * "BEF" "ABEF" etc, but can not use combinations such as "CF" "DF" "ACF" "ADF" etc.
     48      */
     49     private static final int LAST_TOKENS_FOR_INITIALS = 2;
     50     private static final int FIRST_TOKENS_FOR_INITIALS = 2;
     51 
     52     /** The country code of the user's sim card obtained by calling getSimCountryIso*/
     53     private static final String PREF_USER_SIM_COUNTRY_CODE =
     54             "DialtactsActivity_user_sim_country_code";
     55     private static final String PREF_USER_SIM_COUNTRY_CODE_DEFAULT = null;
     56     private static String sUserSimCountryCode = PREF_USER_SIM_COUNTRY_CODE_DEFAULT;
     57 
     58     /** Indicates whether user is in NANP regions.*/
     59     private static boolean sUserInNanpRegion = false;
     60 
     61     /** Set of country names that use NANP code.*/
     62     private static Set<String> sNanpCountries = null;
     63 
     64     /** Set of supported country codes in front of the phone number. */
     65     private static Set<String> sCountryCodes = null;
     66 
     67     /** Dialpad mapping. */
     68     private static final SmartDialMap mMap = new LatinSmartDialMap();
     69 
     70     private static boolean sNanpInitialized = false;
     71 
     72     /** Initializes the Nanp settings, and finds out whether user is in a NANP region.*/
     73     public static void initializeNanpSettings(Context context){
     74         final TelephonyManager manager = (TelephonyManager) context.getSystemService(
     75                 Context.TELEPHONY_SERVICE);
     76         if (manager != null) {
     77             sUserSimCountryCode = manager.getSimCountryIso();
     78         }
     79 
     80         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
     81 
     82         if (sUserSimCountryCode != null) {
     83             /** Updates shared preferences with the latest country obtained from getSimCountryIso.*/
     84             prefs.edit().putString(PREF_USER_SIM_COUNTRY_CODE, sUserSimCountryCode).apply();
     85         } else {
     86             /** Uses previously stored country code if loading fails. */
     87             sUserSimCountryCode = prefs.getString(PREF_USER_SIM_COUNTRY_CODE,
     88                     PREF_USER_SIM_COUNTRY_CODE_DEFAULT);
     89         }
     90         /** Queries the NANP country list to find out whether user is in a NANP region.*/
     91         sUserInNanpRegion = isCountryNanp(sUserSimCountryCode);
     92         sNanpInitialized = true;
     93     }
     94 
     95     /**
     96      * Explicitly setting the user Nanp to the given boolean
     97      */
     98     @VisibleForTesting
     99     public static void setUserInNanpRegion(boolean userInNanpRegion) {
    100         sUserInNanpRegion = userInNanpRegion;
    101     }
    102 
    103     /**
    104      * Class to record phone number parsing information.
    105      */
    106     public static class PhoneNumberTokens {
    107         /** Country code of the phone number. */
    108         final String countryCode;
    109 
    110         /** Offset of national number after the country code. */
    111         final int countryCodeOffset;
    112 
    113         /** Offset of local number after NANP area code.*/
    114         final int nanpCodeOffset;
    115 
    116         public PhoneNumberTokens(String countryCode, int countryCodeOffset, int nanpCodeOffset) {
    117             this.countryCode = countryCode;
    118             this.countryCodeOffset = countryCodeOffset;
    119             this.nanpCodeOffset = nanpCodeOffset;
    120         }
    121     }
    122 
    123     /**
    124      * Parses a contact's name into a list of separated tokens.
    125      *
    126      * @param contactName Contact's name stored in string.
    127      * @return A list of name tokens, for example separated first names, last name, etc.
    128      */
    129     public static ArrayList<String> parseToIndexTokens(String contactName) {
    130         final int length = contactName.length();
    131         final ArrayList<String> result = Lists.newArrayList();
    132         char c;
    133         final StringBuilder currentIndexToken = new StringBuilder();
    134         /**
    135          * Iterates through the whole name string. If the current character is a valid character,
    136          * append it to the current token. If the current character is not a valid character, for
    137          * example space " ", mark the current token as complete and add it to the list of tokens.
    138          */
    139         for (int i = 0; i < length; i++) {
    140             c = mMap.normalizeCharacter(contactName.charAt(i));
    141             if (mMap.isValidDialpadCharacter(c)) {
    142                 /** Converts a character into the number on dialpad that represents the character.*/
    143                 currentIndexToken.append(mMap.getDialpadIndex(c));
    144             } else {
    145                 if (currentIndexToken.length() != 0) {
    146                     result.add(currentIndexToken.toString());
    147                 }
    148                 currentIndexToken.delete(0, currentIndexToken.length());
    149             }
    150         }
    151 
    152         /** Adds the last token in case it has not been added.*/
    153         if (currentIndexToken.length() != 0) {
    154             result.add(currentIndexToken.toString());
    155         }
    156         return result;
    157     }
    158 
    159     /**
    160      * Generates a list of strings that any prefix of any string in the list can be used to look
    161      * up the contact's name.
    162      *
    163      * @param index The contact's name in string.
    164      * @return A List of strings, whose prefix can be used to look up the contact.
    165      */
    166     public static ArrayList<String> generateNamePrefixes(String index) {
    167         final ArrayList<String> result = Lists.newArrayList();
    168 
    169         /** Parses the name into a list of tokens.*/
    170         final ArrayList<String> indexTokens = parseToIndexTokens(index);
    171 
    172         if (indexTokens.size() > 0) {
    173             /** Adds the full token combinations to the list. For example, a contact with name
    174              * "Albert Ben Ed Foster" can be looked up by any prefix of the following strings
    175              * "Foster" "EdFoster" "BenEdFoster" and "AlbertBenEdFoster". This covers all cases of
    176              * look up that contains only one token, and that spans multiple continuous tokens.
    177              */
    178             final StringBuilder fullNameToken = new StringBuilder();
    179             for (int i = indexTokens.size() - 1; i >= 0; i--) {
    180                 fullNameToken.insert(0, indexTokens.get(i));
    181                 result.add(fullNameToken.toString());
    182             }
    183 
    184             /** Adds initial combinations to the list, with the number of initials restricted by
    185              * {@link #LAST_TOKENS_FOR_INITIALS} and {@link #FIRST_TOKENS_FOR_INITIALS}.
    186              * For example, a contact with name "Albert Ben Ed Foster" can be looked up by any
    187              * prefix of the following strings "EFoster" "BFoster" "BEFoster" "AFoster" "ABFoster"
    188              * "AEFoster" and "ABEFoster". This covers all cases of initial lookup.
    189              */
    190             ArrayList<String> fullNames = Lists.newArrayList();
    191             fullNames.add(indexTokens.get(indexTokens.size() - 1));
    192             final int recursiveNameStart = result.size();
    193             int recursiveNameEnd = result.size();
    194             String initial = "";
    195             for (int i = indexTokens.size() - 2; i >= 0; i--) {
    196                 if ((i >= indexTokens.size() - LAST_TOKENS_FOR_INITIALS) ||
    197                         (i < FIRST_TOKENS_FOR_INITIALS)) {
    198                     initial = indexTokens.get(i).substring(0, 1);
    199 
    200                     /** Recursively adds initial combinations to the list.*/
    201                     for (int j = 0; j < fullNames.size(); ++j) {
    202                         result.add(initial + fullNames.get(j));
    203                     }
    204                     for (int j = recursiveNameStart; j < recursiveNameEnd; ++j) {
    205                        result.add(initial + result.get(j));
    206                     }
    207                     recursiveNameEnd = result.size();
    208                     final String currentFullName = fullNames.get(fullNames.size() - 1);
    209                     fullNames.add(indexTokens.get(i) +  currentFullName);
    210                 }
    211             }
    212         }
    213 
    214         return result;
    215     }
    216 
    217     /**
    218      * Computes a list of number strings based on tokens of a given phone number. Any prefix
    219      * of any string in the list can be used to look up the phone number. The list include the
    220      * full phone number, the national number if there is a country code in the phone number, and
    221      * the local number if there is an area code in the phone number following the NANP format.
    222      * For example, if a user has phone number +41 71 394 8392, the list will contain 41713948392
    223      * and 713948392. Any prefix to either of the strings can be used to look up the phone number.
    224      * If a user has a phone number +1 555-302-3029 (NANP format), the list will contain
    225      * 15553023029, 5553023029, and 3023029.
    226      *
    227      * @param number String of user's phone number.
    228      * @return A list of strings where any prefix of any entry can be used to look up the number.
    229      */
    230     public static ArrayList<String> parseToNumberTokens(String number) {
    231         final ArrayList<String> result = Lists.newArrayList();
    232         if (!TextUtils.isEmpty(number)) {
    233             /** Adds the full number to the list.*/
    234             result.add(SmartDialNameMatcher.normalizeNumber(number, mMap));
    235 
    236             final PhoneNumberTokens phoneNumberTokens = parsePhoneNumber(number);
    237             if (phoneNumberTokens == null) {
    238                 return result;
    239             }
    240 
    241             if (phoneNumberTokens.countryCodeOffset != 0) {
    242                 result.add(SmartDialNameMatcher.normalizeNumber(number,
    243                         phoneNumberTokens.countryCodeOffset, mMap));
    244             }
    245 
    246             if (phoneNumberTokens.nanpCodeOffset != 0) {
    247                 result.add(SmartDialNameMatcher.normalizeNumber(number,
    248                         phoneNumberTokens.nanpCodeOffset, mMap));
    249             }
    250         }
    251         return result;
    252     }
    253 
    254     /**
    255      * Parses a phone number to find out whether it has country code and NANP area code.
    256      *
    257      * @param number Raw phone number.
    258      * @return a PhoneNumberToken instance with country code, NANP code information.
    259      */
    260     public static PhoneNumberTokens parsePhoneNumber(String number) {
    261         String countryCode = "";
    262         int countryCodeOffset = 0;
    263         int nanpNumberOffset = 0;
    264 
    265         if (!TextUtils.isEmpty(number)) {
    266             String normalizedNumber = SmartDialNameMatcher.normalizeNumber(number, mMap);
    267             if (number.charAt(0) == '+') {
    268                 /** If the number starts with '+', tries to find valid country code. */
    269                 for (int i = 1; i <= 1 + 3; i++) {
    270                     if (number.length() <= i) {
    271                         break;
    272                     }
    273                     countryCode = number.substring(1, i);
    274                     if (isValidCountryCode(countryCode)) {
    275                         countryCodeOffset = i;
    276                         break;
    277                     }
    278                 }
    279             } else {
    280                 /** If the number does not start with '+', finds out whether it is in NANP
    281                  * format and has '1' preceding the number.
    282                  */
    283                 if ((normalizedNumber.length() == 11) && (normalizedNumber.charAt(0) == '1') &&
    284                         (sUserInNanpRegion)) {
    285                     countryCode = "1";
    286                     countryCodeOffset = number.indexOf(normalizedNumber.charAt(1));
    287                     if (countryCodeOffset == -1) {
    288                         countryCodeOffset = 0;
    289                     }
    290                 }
    291             }
    292 
    293             /** If user is in NANP region, finds out whether a number is in NANP format.*/
    294             if (sUserInNanpRegion)  {
    295                 String areaCode = "";
    296                 if (countryCode.equals("") && normalizedNumber.length() == 10){
    297                     /** if the number has no country code but fits the NANP format, extracts the
    298                      * NANP area code, and finds out offset of the local number.
    299                      */
    300                     areaCode = normalizedNumber.substring(0, 3);
    301                 } else if (countryCode.equals("1") && normalizedNumber.length() == 11) {
    302                     /** If the number has country code '1', finds out area code and offset of the
    303                      * local number.
    304                      */
    305                     areaCode = normalizedNumber.substring(1, 4);
    306                 }
    307                 if (!areaCode.equals("")) {
    308                     final int areaCodeIndex = number.indexOf(areaCode);
    309                     if (areaCodeIndex != -1) {
    310                         nanpNumberOffset = number.indexOf(areaCode) + 3;
    311                     }
    312                 }
    313             }
    314         }
    315         return new PhoneNumberTokens(countryCode, countryCodeOffset, nanpNumberOffset);
    316     }
    317 
    318     /**
    319      * Checkes whether a country code is valid.
    320      */
    321     private static boolean isValidCountryCode(String countryCode) {
    322         if (sCountryCodes == null) {
    323             sCountryCodes = initCountryCodes();
    324         }
    325         return sCountryCodes.contains(countryCode);
    326     }
    327 
    328     private static Set<String> initCountryCodes() {
    329         final HashSet<String> result = new HashSet<String>();
    330         result.add("1");
    331         result.add("7");
    332         result.add("20");
    333         result.add("27");
    334         result.add("30");
    335         result.add("31");
    336         result.add("32");
    337         result.add("33");
    338         result.add("34");
    339         result.add("36");
    340         result.add("39");
    341         result.add("40");
    342         result.add("41");
    343         result.add("43");
    344         result.add("44");
    345         result.add("45");
    346         result.add("46");
    347         result.add("47");
    348         result.add("48");
    349         result.add("49");
    350         result.add("51");
    351         result.add("52");
    352         result.add("53");
    353         result.add("54");
    354         result.add("55");
    355         result.add("56");
    356         result.add("57");
    357         result.add("58");
    358         result.add("60");
    359         result.add("61");
    360         result.add("62");
    361         result.add("63");
    362         result.add("64");
    363         result.add("65");
    364         result.add("66");
    365         result.add("81");
    366         result.add("82");
    367         result.add("84");
    368         result.add("86");
    369         result.add("90");
    370         result.add("91");
    371         result.add("92");
    372         result.add("93");
    373         result.add("94");
    374         result.add("95");
    375         result.add("98");
    376         result.add("211");
    377         result.add("212");
    378         result.add("213");
    379         result.add("216");
    380         result.add("218");
    381         result.add("220");
    382         result.add("221");
    383         result.add("222");
    384         result.add("223");
    385         result.add("224");
    386         result.add("225");
    387         result.add("226");
    388         result.add("227");
    389         result.add("228");
    390         result.add("229");
    391         result.add("230");
    392         result.add("231");
    393         result.add("232");
    394         result.add("233");
    395         result.add("234");
    396         result.add("235");
    397         result.add("236");
    398         result.add("237");
    399         result.add("238");
    400         result.add("239");
    401         result.add("240");
    402         result.add("241");
    403         result.add("242");
    404         result.add("243");
    405         result.add("244");
    406         result.add("245");
    407         result.add("246");
    408         result.add("247");
    409         result.add("248");
    410         result.add("249");
    411         result.add("250");
    412         result.add("251");
    413         result.add("252");
    414         result.add("253");
    415         result.add("254");
    416         result.add("255");
    417         result.add("256");
    418         result.add("257");
    419         result.add("258");
    420         result.add("260");
    421         result.add("261");
    422         result.add("262");
    423         result.add("263");
    424         result.add("264");
    425         result.add("265");
    426         result.add("266");
    427         result.add("267");
    428         result.add("268");
    429         result.add("269");
    430         result.add("290");
    431         result.add("291");
    432         result.add("297");
    433         result.add("298");
    434         result.add("299");
    435         result.add("350");
    436         result.add("351");
    437         result.add("352");
    438         result.add("353");
    439         result.add("354");
    440         result.add("355");
    441         result.add("356");
    442         result.add("357");
    443         result.add("358");
    444         result.add("359");
    445         result.add("370");
    446         result.add("371");
    447         result.add("372");
    448         result.add("373");
    449         result.add("374");
    450         result.add("375");
    451         result.add("376");
    452         result.add("377");
    453         result.add("378");
    454         result.add("379");
    455         result.add("380");
    456         result.add("381");
    457         result.add("382");
    458         result.add("385");
    459         result.add("386");
    460         result.add("387");
    461         result.add("389");
    462         result.add("420");
    463         result.add("421");
    464         result.add("423");
    465         result.add("500");
    466         result.add("501");
    467         result.add("502");
    468         result.add("503");
    469         result.add("504");
    470         result.add("505");
    471         result.add("506");
    472         result.add("507");
    473         result.add("508");
    474         result.add("509");
    475         result.add("590");
    476         result.add("591");
    477         result.add("592");
    478         result.add("593");
    479         result.add("594");
    480         result.add("595");
    481         result.add("596");
    482         result.add("597");
    483         result.add("598");
    484         result.add("599");
    485         result.add("670");
    486         result.add("672");
    487         result.add("673");
    488         result.add("674");
    489         result.add("675");
    490         result.add("676");
    491         result.add("677");
    492         result.add("678");
    493         result.add("679");
    494         result.add("680");
    495         result.add("681");
    496         result.add("682");
    497         result.add("683");
    498         result.add("685");
    499         result.add("686");
    500         result.add("687");
    501         result.add("688");
    502         result.add("689");
    503         result.add("690");
    504         result.add("691");
    505         result.add("692");
    506         result.add("800");
    507         result.add("808");
    508         result.add("850");
    509         result.add("852");
    510         result.add("853");
    511         result.add("855");
    512         result.add("856");
    513         result.add("870");
    514         result.add("878");
    515         result.add("880");
    516         result.add("881");
    517         result.add("882");
    518         result.add("883");
    519         result.add("886");
    520         result.add("888");
    521         result.add("960");
    522         result.add("961");
    523         result.add("962");
    524         result.add("963");
    525         result.add("964");
    526         result.add("965");
    527         result.add("966");
    528         result.add("967");
    529         result.add("968");
    530         result.add("970");
    531         result.add("971");
    532         result.add("972");
    533         result.add("973");
    534         result.add("974");
    535         result.add("975");
    536         result.add("976");
    537         result.add("977");
    538         result.add("979");
    539         result.add("992");
    540         result.add("993");
    541         result.add("994");
    542         result.add("995");
    543         result.add("996");
    544         result.add("998");
    545         return result;
    546     }
    547 
    548     public static SmartDialMap getMap() {
    549         return mMap;
    550     }
    551 
    552     /**
    553      * Indicates whether the given country uses NANP numbers
    554      * @see <a href="https://en.wikipedia.org/wiki/North_American_Numbering_Plan">
    555      *     https://en.wikipedia.org/wiki/North_American_Numbering_Plan</a>
    556      *
    557      * @param country ISO 3166 country code (case doesn't matter)
    558      * @return True if country uses NANP numbers (e.g. US, Canada), false otherwise
    559      */
    560     @VisibleForTesting
    561     public static boolean isCountryNanp(String country) {
    562         if (TextUtils.isEmpty(country)) {
    563             return false;
    564         }
    565         if (sNanpCountries == null) {
    566             sNanpCountries = initNanpCountries();
    567         }
    568         return sNanpCountries.contains(country.toUpperCase());
    569     }
    570 
    571     private static Set<String> initNanpCountries() {
    572         final HashSet<String> result = new HashSet<String>();
    573         result.add("US"); // United States
    574         result.add("CA"); // Canada
    575         result.add("AS"); // American Samoa
    576         result.add("AI"); // Anguilla
    577         result.add("AG"); // Antigua and Barbuda
    578         result.add("BS"); // Bahamas
    579         result.add("BB"); // Barbados
    580         result.add("BM"); // Bermuda
    581         result.add("VG"); // British Virgin Islands
    582         result.add("KY"); // Cayman Islands
    583         result.add("DM"); // Dominica
    584         result.add("DO"); // Dominican Republic
    585         result.add("GD"); // Grenada
    586         result.add("GU"); // Guam
    587         result.add("JM"); // Jamaica
    588         result.add("PR"); // Puerto Rico
    589         result.add("MS"); // Montserrat
    590         result.add("MP"); // Northern Mariana Islands
    591         result.add("KN"); // Saint Kitts and Nevis
    592         result.add("LC"); // Saint Lucia
    593         result.add("VC"); // Saint Vincent and the Grenadines
    594         result.add("TT"); // Trinidad and Tobago
    595         result.add("TC"); // Turks and Caicos Islands
    596         result.add("VI"); // U.S. Virgin Islands
    597         return result;
    598     }
    599 
    600     /**
    601      * Returns whether the user is in a region that uses Nanp format based on the sim location.
    602      *
    603      * @return Whether user is in Nanp region.
    604      */
    605     public static boolean getUserInNanpRegion() {
    606         return sUserInNanpRegion;
    607     }
    608 }
    609