/* * 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.smartdial.util; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.support.annotation.VisibleForTesting; import android.telephony.TelephonyManager; import android.text.TextUtils; import com.android.dialer.smartdial.map.CompositeSmartDialMap; import java.util.ArrayList; import java.util.HashSet; import java.util.Set; /** * Smart Dial utility class to find prefixes of contacts. It contains both methods to find supported * prefix combinations for contact names, and also methods to find supported prefix combinations for * contacts' phone numbers. Each contact name is separated into several tokens, such as first name, * middle name, family name etc. Each phone number is also separated into country code, NANP area * code, and local number if such separation is possible. */ public class SmartDialPrefix { /** * The number of starting and ending tokens in a contact's name considered for initials. For * example, if both constants are set to 2, and a contact's name is "Albert Ben Charles Daniel Ed * Foster", the first two tokens "Albert" "Ben", and last two tokens "Ed" "Foster" can be replaced * by their initials in contact name matching. Users can look up this contact by combinations of * his initials such as "AF" "BF" "EF" "ABF" "BEF" "ABEF" etc, but can not use combinations such * as "CF" "DF" "ACF" "ADF" etc. */ private static final int LAST_TOKENS_FOR_INITIALS = 2; private static final int FIRST_TOKENS_FOR_INITIALS = 2; /** 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 userSimCountryCode = PREF_USER_SIM_COUNTRY_CODE_DEFAULT; /** Indicates whether user is in NANP regions. */ private static boolean userInNanpRegion = false; /** Set of country names that use NANP code. */ private static Set nanpCountries = null; /** Set of supported country codes in front of the phone number. */ private static Set countryCodes = null; private static boolean nanpInitialized = false; /** Initializes the Nanp settings, and finds out whether user is in a NANP region. */ public static void initializeNanpSettings(Context context) { final TelephonyManager manager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); if (manager != null) { userSimCountryCode = manager.getSimCountryIso(); } final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); if (userSimCountryCode != null) { /** Updates shared preferences with the latest country obtained from getSimCountryIso. */ prefs.edit().putString(PREF_USER_SIM_COUNTRY_CODE, userSimCountryCode).apply(); } else { /** Uses previously stored country code if loading fails. */ userSimCountryCode = prefs.getString(PREF_USER_SIM_COUNTRY_CODE, PREF_USER_SIM_COUNTRY_CODE_DEFAULT); } /** Queries the NANP country list to find out whether user is in a NANP region. */ userInNanpRegion = isCountryNanp(userSimCountryCode); nanpInitialized = true; } /** * Parses a contact's name into a list of separated tokens. * * @param contactName Contact's name stored in string. * @return A list of name tokens, for example separated first names, last name, etc. */ public static ArrayList parseToIndexTokens(Context context, String contactName) { final int length = contactName.length(); final ArrayList result = new ArrayList<>(); char c; final StringBuilder currentIndexToken = new StringBuilder(); /** * Iterates through the whole name string. If the current character is a valid character, append * it to the current token. If the current character is not a valid character, for example space * " ", mark the current token as complete and add it to the list of tokens. */ for (int i = 0; i < length; i++) { c = CompositeSmartDialMap.normalizeCharacter(context, contactName.charAt(i)); if (CompositeSmartDialMap.isValidDialpadCharacter(context, c)) { /** Converts a character into the number on dialpad that represents the character. */ currentIndexToken.append(CompositeSmartDialMap.getDialpadIndex(context, c)); } else { if (currentIndexToken.length() != 0) { result.add(currentIndexToken.toString()); } currentIndexToken.delete(0, currentIndexToken.length()); } } /** Adds the last token in case it has not been added. */ if (currentIndexToken.length() != 0) { result.add(currentIndexToken.toString()); } return result; } /** * Generates a list of strings that any prefix of any string in the list can be used to look up * the contact's name. * * @param index The contact's name in string. * @return A List of strings, whose prefix can be used to look up the contact. */ public static ArrayList generateNamePrefixes(Context context, String index) { final ArrayList result = new ArrayList<>(); /** Parses the name into a list of tokens. */ final ArrayList indexTokens = parseToIndexTokens(context, index); if (indexTokens.size() > 0) { /** * Adds the full token combinations to the list. For example, a contact with name "Albert Ben * Ed Foster" can be looked up by any prefix of the following strings "Foster" "EdFoster" * "BenEdFoster" and "AlbertBenEdFoster". This covers all cases of look up that contains only * one token, and that spans multiple continuous tokens. */ final StringBuilder fullNameToken = new StringBuilder(); for (int i = indexTokens.size() - 1; i >= 0; i--) { fullNameToken.insert(0, indexTokens.get(i)); result.add(fullNameToken.toString()); } /** * Adds initial combinations to the list, with the number of initials restricted by {@link * #LAST_TOKENS_FOR_INITIALS} and {@link #FIRST_TOKENS_FOR_INITIALS}. For example, a contact * with name "Albert Ben Ed Foster" can be looked up by any prefix of the following strings * "EFoster" "BFoster" "BEFoster" "AFoster" "ABFoster" "AEFoster" and "ABEFoster". This covers * all cases of initial lookup. */ ArrayList fullNames = new ArrayList<>(); fullNames.add(indexTokens.get(indexTokens.size() - 1)); final int recursiveNameStart = result.size(); int recursiveNameEnd = result.size(); String initial = ""; for (int i = indexTokens.size() - 2; i >= 0; i--) { if ((i >= indexTokens.size() - LAST_TOKENS_FOR_INITIALS) || (i < FIRST_TOKENS_FOR_INITIALS)) { initial = indexTokens.get(i).substring(0, 1); /** Recursively adds initial combinations to the list. */ for (int j = 0; j < fullNames.size(); ++j) { result.add(initial + fullNames.get(j)); } for (int j = recursiveNameStart; j < recursiveNameEnd; ++j) { result.add(initial + result.get(j)); } recursiveNameEnd = result.size(); final String currentFullName = fullNames.get(fullNames.size() - 1); fullNames.add(indexTokens.get(i) + currentFullName); } } } return result; } /** * Computes a list of number strings based on tokens of a given phone number. Any prefix of any * string in the list can be used to look up the phone number. The list include the full phone * number, the national number if there is a country code in the phone number, and the local * number if there is an area code in the phone number following the NANP format. For example, if * a user has phone number +41 71 394 8392, the list will contain 41713948392 and 713948392. Any * prefix to either of the strings can be used to look up the phone number. If a user has a phone * number +1 555-302-3029 (NANP format), the list will contain 15553023029, 5553023029, and * 3023029. * * @param number String of user's phone number. * @return A list of strings where any prefix of any entry can be used to look up the number. */ public static ArrayList parseToNumberTokens(Context context, String number) { final ArrayList result = new ArrayList<>(); if (!TextUtils.isEmpty(number)) { /** Adds the full number to the list. */ result.add(SmartDialNameMatcher.normalizeNumber(context, number)); final PhoneNumberTokens phoneNumberTokens = parsePhoneNumber(context, number); if (phoneNumberTokens == null) { return result; } if (phoneNumberTokens.countryCodeOffset != 0) { result.add( SmartDialNameMatcher.normalizeNumber( context, number, phoneNumberTokens.countryCodeOffset)); } if (phoneNumberTokens.nanpCodeOffset != 0) { result.add( SmartDialNameMatcher.normalizeNumber( context, number, phoneNumberTokens.nanpCodeOffset)); } } return result; } /** * Parses a phone number to find out whether it has country code and NANP area code. * * @param number Raw phone number. * @return a PhoneNumberToken instance with country code, NANP code information. */ public static PhoneNumberTokens parsePhoneNumber(Context context, String number) { String countryCode = ""; int countryCodeOffset = 0; int nanpNumberOffset = 0; if (!TextUtils.isEmpty(number)) { String normalizedNumber = SmartDialNameMatcher.normalizeNumber(context, number); if (number.charAt(0) == '+') { /** If the number starts with '+', tries to find valid country code. */ for (int i = 1; i <= 1 + 3; i++) { if (number.length() <= i) { break; } countryCode = number.substring(1, i); if (isValidCountryCode(countryCode)) { countryCodeOffset = i; break; } } } else { /** * If the number does not start with '+', finds out whether it is in NANP format and has '1' * preceding the number. */ if ((normalizedNumber.length() == 11) && (normalizedNumber.charAt(0) == '1') && (userInNanpRegion)) { countryCode = "1"; countryCodeOffset = number.indexOf(normalizedNumber.charAt(1)); if (countryCodeOffset == -1) { countryCodeOffset = 0; } } } /** If user is in NANP region, finds out whether a number is in NANP format. */ if (userInNanpRegion) { String areaCode = ""; if (countryCode.equals("") && normalizedNumber.length() == 10) { /** * if the number has no country code but fits the NANP format, extracts the NANP area * code, and finds out offset of the local number. */ areaCode = normalizedNumber.substring(0, 3); } else if (countryCode.equals("1") && normalizedNumber.length() == 11) { /** * If the number has country code '1', finds out area code and offset of the local number. */ areaCode = normalizedNumber.substring(1, 4); } if (!areaCode.equals("")) { final int areaCodeIndex = number.indexOf(areaCode); if (areaCodeIndex != -1) { nanpNumberOffset = number.indexOf(areaCode) + 3; } } } } return new PhoneNumberTokens(countryCode, countryCodeOffset, nanpNumberOffset); } /** Checkes whether a country code is valid. */ private static boolean isValidCountryCode(String countryCode) { if (countryCodes == null) { countryCodes = initCountryCodes(); } return countryCodes.contains(countryCode); } private static Set initCountryCodes() { final HashSet result = new HashSet(); 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; } /** * 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 * @see * https://en.wikipedia.org/wiki/North_American_Numbering_Plan */ @VisibleForTesting public static boolean isCountryNanp(String country) { if (TextUtils.isEmpty(country)) { return false; } if (nanpCountries == null) { nanpCountries = initNanpCountries(); } return nanpCountries.contains(country.toUpperCase()); } private static Set initNanpCountries() { final HashSet result = new HashSet(); 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; } /** * Returns whether the user is in a region that uses Nanp format based on the sim location. * * @return Whether user is in Nanp region. */ public static boolean getUserInNanpRegion() { return userInNanpRegion; } /** Explicitly setting the user Nanp to the given boolean */ @VisibleForTesting public static void setUserInNanpRegion(boolean userInNanpRegion) { SmartDialPrefix.userInNanpRegion = userInNanpRegion; } /** Class to record phone number parsing information. */ public static class PhoneNumberTokens { /** Country code of the phone number. */ final String countryCode; /** Offset of national number after the country code. */ final int countryCodeOffset; /** Offset of local number after NANP area code. */ final int nanpCodeOffset; public PhoneNumberTokens(String countryCode, int countryCodeOffset, int nanpCodeOffset) { this.countryCode = countryCode; this.countryCodeOffset = countryCodeOffset; this.nanpCodeOffset = nanpCodeOffset; } } }