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