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.*/
initializeNanpSettings(Context context)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
setUserInNanpRegion(boolean userInNanpRegion)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 
PhoneNumberTokens(String countryCode, int countryCodeOffset, int nanpCodeOffset)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      */
parseToIndexTokens(String contactName)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      */
generateNamePrefixes(String index)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      */
parseToNumberTokens(String number)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      */
parsePhoneNumber(String number)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      */
isValidCountryCode(String countryCode)321     private static boolean isValidCountryCode(String countryCode) {
322         if (sCountryCodes == null) {
323             sCountryCodes = initCountryCodes();
324         }
325         return sCountryCodes.contains(countryCode);
326     }
327 
initCountryCodes()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 
getMap()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
isCountryNanp(String country)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 
initNanpCountries()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      */
getUserInNanpRegion()605     public static boolean getUserInNanpRegion() {
606         return sUserInNanpRegion;
607     }
608 }
609