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. */
initializeNanpSettings(Context context)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    */
parseToIndexTokens(String contactName)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    */
generateNamePrefixes(String index)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    */
parseToNumberTokens(String number)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    */
parsePhoneNumber(String number)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. */
isValidCountryCode(String countryCode)293   private static boolean isValidCountryCode(String countryCode) {
294     if (sCountryCodes == null) {
295       sCountryCodes = initCountryCodes();
296     }
297     return sCountryCodes.contains(countryCode);
298   }
299 
initCountryCodes()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 
getMap()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
isCountryNanp(String country)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 
initNanpCountries()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    */
getUserInNanpRegion()577   public static boolean getUserInNanpRegion() {
578     return sUserInNanpRegion;
579   }
580 
581   /** Explicitly setting the user Nanp to the given boolean */
582   @VisibleForTesting
setUserInNanpRegion(boolean userInNanpRegion)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 
PhoneNumberTokens(String countryCode, int countryCodeOffset, int nanpCodeOffset)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