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
17package com.android.dialer.smartdial.util;
18
19import android.content.Context;
20import android.content.SharedPreferences;
21import android.preference.PreferenceManager;
22import android.support.annotation.VisibleForTesting;
23import android.telephony.TelephonyManager;
24import android.text.TextUtils;
25import com.android.dialer.smartdial.map.CompositeSmartDialMap;
26import java.util.ArrayList;
27import java.util.HashSet;
28import java.util.Set;
29
30/**
31 * Smart Dial utility class to find prefixes of contacts. It contains both methods to find supported
32 * prefix combinations for contact names, and also methods to find supported prefix combinations for
33 * contacts' phone numbers. Each contact name is separated into several tokens, such as first name,
34 * middle name, family name etc. Each phone number is also separated into country code, NANP area
35 * code, and local number if such separation is possible.
36 */
37public class SmartDialPrefix {
38
39  /**
40   * The number of starting and ending tokens in a contact's name considered for initials. For
41   * example, if both constants are set to 2, and a contact's name is "Albert Ben Charles Daniel Ed
42   * Foster", the first two tokens "Albert" "Ben", and last two tokens "Ed" "Foster" can be replaced
43   * by their initials in contact name matching. Users can look up this contact by combinations of
44   * his initials such as "AF" "BF" "EF" "ABF" "BEF" "ABEF" etc, but can not use combinations such
45   * as "CF" "DF" "ACF" "ADF" etc.
46   */
47  private static final int LAST_TOKENS_FOR_INITIALS = 2;
48
49  private static final int FIRST_TOKENS_FOR_INITIALS = 2;
50
51  /** The country code of the user's sim card obtained by calling getSimCountryIso */
52  private static final String PREF_USER_SIM_COUNTRY_CODE =
53      "DialtactsActivity_user_sim_country_code";
54
55  private static final String PREF_USER_SIM_COUNTRY_CODE_DEFAULT = null;
56
57  private static String userSimCountryCode = PREF_USER_SIM_COUNTRY_CODE_DEFAULT;
58  /** Indicates whether user is in NANP regions. */
59  private static boolean userInNanpRegion = false;
60  /** Set of country names that use NANP code. */
61  private static Set<String> nanpCountries = null;
62  /** Set of supported country codes in front of the phone number. */
63  private static Set<String> countryCodes = null;
64
65  private static boolean nanpInitialized = false;
66
67  /** Initializes the Nanp settings, and finds out whether user is in a NANP region. */
68  public static void initializeNanpSettings(Context context) {
69    final TelephonyManager manager =
70        (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
71    if (manager != null) {
72      userSimCountryCode = manager.getSimCountryIso();
73    }
74
75    final SharedPreferences prefs =
76        PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
77
78    if (userSimCountryCode != null) {
79      /** Updates shared preferences with the latest country obtained from getSimCountryIso. */
80      prefs.edit().putString(PREF_USER_SIM_COUNTRY_CODE, userSimCountryCode).apply();
81    } else {
82      /** Uses previously stored country code if loading fails. */
83      userSimCountryCode =
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    userInNanpRegion = isCountryNanp(userSimCountryCode);
88    nanpInitialized = 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(Context context, 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 = CompositeSmartDialMap.normalizeCharacter(context, contactName.charAt(i));
109      if (CompositeSmartDialMap.isValidDialpadCharacter(context, c)) {
110        /** Converts a character into the number on dialpad that represents the character. */
111        currentIndexToken.append(CompositeSmartDialMap.getDialpadIndex(context, 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(Context context, 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(context, 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(Context context, 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(context, number));
205
206      final PhoneNumberTokens phoneNumberTokens = parsePhoneNumber(context, number);
207      if (phoneNumberTokens == null) {
208        return result;
209      }
210
211      if (phoneNumberTokens.countryCodeOffset != 0) {
212        result.add(
213            SmartDialNameMatcher.normalizeNumber(
214                context, number, phoneNumberTokens.countryCodeOffset));
215      }
216
217      if (phoneNumberTokens.nanpCodeOffset != 0) {
218        result.add(
219            SmartDialNameMatcher.normalizeNumber(
220                context, number, phoneNumberTokens.nanpCodeOffset));
221      }
222    }
223    return result;
224  }
225
226  /**
227   * Parses a phone number to find out whether it has country code and NANP area code.
228   *
229   * @param number Raw phone number.
230   * @return a PhoneNumberToken instance with country code, NANP code information.
231   */
232  public static PhoneNumberTokens parsePhoneNumber(Context context, String number) {
233    String countryCode = "";
234    int countryCodeOffset = 0;
235    int nanpNumberOffset = 0;
236
237    if (!TextUtils.isEmpty(number)) {
238      String normalizedNumber = SmartDialNameMatcher.normalizeNumber(context, number);
239      if (number.charAt(0) == '+') {
240        /** If the number starts with '+', tries to find valid country code. */
241        for (int i = 1; i <= 1 + 3; i++) {
242          if (number.length() <= i) {
243            break;
244          }
245          countryCode = number.substring(1, i);
246          if (isValidCountryCode(countryCode)) {
247            countryCodeOffset = i;
248            break;
249          }
250        }
251      } else {
252        /**
253         * If the number does not start with '+', finds out whether it is in NANP format and has '1'
254         * preceding the number.
255         */
256        if ((normalizedNumber.length() == 11)
257            && (normalizedNumber.charAt(0) == '1')
258            && (userInNanpRegion)) {
259          countryCode = "1";
260          countryCodeOffset = number.indexOf(normalizedNumber.charAt(1));
261          if (countryCodeOffset == -1) {
262            countryCodeOffset = 0;
263          }
264        }
265      }
266
267      /** If user is in NANP region, finds out whether a number is in NANP format. */
268      if (userInNanpRegion) {
269        String areaCode = "";
270        if (countryCode.equals("") && normalizedNumber.length() == 10) {
271          /**
272           * if the number has no country code but fits the NANP format, extracts the NANP area
273           * code, and finds out offset of the local number.
274           */
275          areaCode = normalizedNumber.substring(0, 3);
276        } else if (countryCode.equals("1") && normalizedNumber.length() == 11) {
277          /**
278           * If the number has country code '1', finds out area code and offset of the local number.
279           */
280          areaCode = normalizedNumber.substring(1, 4);
281        }
282        if (!areaCode.equals("")) {
283          final int areaCodeIndex = number.indexOf(areaCode);
284          if (areaCodeIndex != -1) {
285            nanpNumberOffset = number.indexOf(areaCode) + 3;
286          }
287        }
288      }
289    }
290    return new PhoneNumberTokens(countryCode, countryCodeOffset, nanpNumberOffset);
291  }
292
293  /** Checkes whether a country code is valid. */
294  private static boolean isValidCountryCode(String countryCode) {
295    if (countryCodes == null) {
296      countryCodes = initCountryCodes();
297    }
298    return countryCodes.contains(countryCode);
299  }
300
301  private static Set<String> initCountryCodes() {
302    final HashSet<String> result = new HashSet<String>();
303    result.add("1");
304    result.add("7");
305    result.add("20");
306    result.add("27");
307    result.add("30");
308    result.add("31");
309    result.add("32");
310    result.add("33");
311    result.add("34");
312    result.add("36");
313    result.add("39");
314    result.add("40");
315    result.add("41");
316    result.add("43");
317    result.add("44");
318    result.add("45");
319    result.add("46");
320    result.add("47");
321    result.add("48");
322    result.add("49");
323    result.add("51");
324    result.add("52");
325    result.add("53");
326    result.add("54");
327    result.add("55");
328    result.add("56");
329    result.add("57");
330    result.add("58");
331    result.add("60");
332    result.add("61");
333    result.add("62");
334    result.add("63");
335    result.add("64");
336    result.add("65");
337    result.add("66");
338    result.add("81");
339    result.add("82");
340    result.add("84");
341    result.add("86");
342    result.add("90");
343    result.add("91");
344    result.add("92");
345    result.add("93");
346    result.add("94");
347    result.add("95");
348    result.add("98");
349    result.add("211");
350    result.add("212");
351    result.add("213");
352    result.add("216");
353    result.add("218");
354    result.add("220");
355    result.add("221");
356    result.add("222");
357    result.add("223");
358    result.add("224");
359    result.add("225");
360    result.add("226");
361    result.add("227");
362    result.add("228");
363    result.add("229");
364    result.add("230");
365    result.add("231");
366    result.add("232");
367    result.add("233");
368    result.add("234");
369    result.add("235");
370    result.add("236");
371    result.add("237");
372    result.add("238");
373    result.add("239");
374    result.add("240");
375    result.add("241");
376    result.add("242");
377    result.add("243");
378    result.add("244");
379    result.add("245");
380    result.add("246");
381    result.add("247");
382    result.add("248");
383    result.add("249");
384    result.add("250");
385    result.add("251");
386    result.add("252");
387    result.add("253");
388    result.add("254");
389    result.add("255");
390    result.add("256");
391    result.add("257");
392    result.add("258");
393    result.add("260");
394    result.add("261");
395    result.add("262");
396    result.add("263");
397    result.add("264");
398    result.add("265");
399    result.add("266");
400    result.add("267");
401    result.add("268");
402    result.add("269");
403    result.add("290");
404    result.add("291");
405    result.add("297");
406    result.add("298");
407    result.add("299");
408    result.add("350");
409    result.add("351");
410    result.add("352");
411    result.add("353");
412    result.add("354");
413    result.add("355");
414    result.add("356");
415    result.add("357");
416    result.add("358");
417    result.add("359");
418    result.add("370");
419    result.add("371");
420    result.add("372");
421    result.add("373");
422    result.add("374");
423    result.add("375");
424    result.add("376");
425    result.add("377");
426    result.add("378");
427    result.add("379");
428    result.add("380");
429    result.add("381");
430    result.add("382");
431    result.add("385");
432    result.add("386");
433    result.add("387");
434    result.add("389");
435    result.add("420");
436    result.add("421");
437    result.add("423");
438    result.add("500");
439    result.add("501");
440    result.add("502");
441    result.add("503");
442    result.add("504");
443    result.add("505");
444    result.add("506");
445    result.add("507");
446    result.add("508");
447    result.add("509");
448    result.add("590");
449    result.add("591");
450    result.add("592");
451    result.add("593");
452    result.add("594");
453    result.add("595");
454    result.add("596");
455    result.add("597");
456    result.add("598");
457    result.add("599");
458    result.add("670");
459    result.add("672");
460    result.add("673");
461    result.add("674");
462    result.add("675");
463    result.add("676");
464    result.add("677");
465    result.add("678");
466    result.add("679");
467    result.add("680");
468    result.add("681");
469    result.add("682");
470    result.add("683");
471    result.add("685");
472    result.add("686");
473    result.add("687");
474    result.add("688");
475    result.add("689");
476    result.add("690");
477    result.add("691");
478    result.add("692");
479    result.add("800");
480    result.add("808");
481    result.add("850");
482    result.add("852");
483    result.add("853");
484    result.add("855");
485    result.add("856");
486    result.add("870");
487    result.add("878");
488    result.add("880");
489    result.add("881");
490    result.add("882");
491    result.add("883");
492    result.add("886");
493    result.add("888");
494    result.add("960");
495    result.add("961");
496    result.add("962");
497    result.add("963");
498    result.add("964");
499    result.add("965");
500    result.add("966");
501    result.add("967");
502    result.add("968");
503    result.add("970");
504    result.add("971");
505    result.add("972");
506    result.add("973");
507    result.add("974");
508    result.add("975");
509    result.add("976");
510    result.add("977");
511    result.add("979");
512    result.add("992");
513    result.add("993");
514    result.add("994");
515    result.add("995");
516    result.add("996");
517    result.add("998");
518    return result;
519  }
520
521  /**
522   * Indicates whether the given country uses NANP numbers
523   *
524   * @param country ISO 3166 country code (case doesn't matter)
525   * @return True if country uses NANP numbers (e.g. US, Canada), false otherwise
526   * @see <a href="https://en.wikipedia.org/wiki/North_American_Numbering_Plan">
527   *     https://en.wikipedia.org/wiki/North_American_Numbering_Plan</a>
528   */
529  @VisibleForTesting
530  public static boolean isCountryNanp(String country) {
531    if (TextUtils.isEmpty(country)) {
532      return false;
533    }
534    if (nanpCountries == null) {
535      nanpCountries = initNanpCountries();
536    }
537    return nanpCountries.contains(country.toUpperCase());
538  }
539
540  private static Set<String> initNanpCountries() {
541    final HashSet<String> result = new HashSet<String>();
542    result.add("US"); // United States
543    result.add("CA"); // Canada
544    result.add("AS"); // American Samoa
545    result.add("AI"); // Anguilla
546    result.add("AG"); // Antigua and Barbuda
547    result.add("BS"); // Bahamas
548    result.add("BB"); // Barbados
549    result.add("BM"); // Bermuda
550    result.add("VG"); // British Virgin Islands
551    result.add("KY"); // Cayman Islands
552    result.add("DM"); // Dominica
553    result.add("DO"); // Dominican Republic
554    result.add("GD"); // Grenada
555    result.add("GU"); // Guam
556    result.add("JM"); // Jamaica
557    result.add("PR"); // Puerto Rico
558    result.add("MS"); // Montserrat
559    result.add("MP"); // Northern Mariana Islands
560    result.add("KN"); // Saint Kitts and Nevis
561    result.add("LC"); // Saint Lucia
562    result.add("VC"); // Saint Vincent and the Grenadines
563    result.add("TT"); // Trinidad and Tobago
564    result.add("TC"); // Turks and Caicos Islands
565    result.add("VI"); // U.S. Virgin Islands
566    return result;
567  }
568
569  /**
570   * Returns whether the user is in a region that uses Nanp format based on the sim location.
571   *
572   * @return Whether user is in Nanp region.
573   */
574  public static boolean getUserInNanpRegion() {
575    return userInNanpRegion;
576  }
577
578  /** Explicitly setting the user Nanp to the given boolean */
579  @VisibleForTesting
580  public static void setUserInNanpRegion(boolean userInNanpRegion) {
581    SmartDialPrefix.userInNanpRegion = userInNanpRegion;
582  }
583
584  /** Class to record phone number parsing information. */
585  public static class PhoneNumberTokens {
586
587    /** Country code of the phone number. */
588    final String countryCode;
589
590    /** Offset of national number after the country code. */
591    final int countryCodeOffset;
592
593    /** Offset of local number after NANP area code. */
594    final int nanpCodeOffset;
595
596    public PhoneNumberTokens(String countryCode, int countryCodeOffset, int nanpCodeOffset) {
597      this.countryCode = countryCode;
598      this.countryCodeOffset = countryCodeOffset;
599      this.nanpCodeOffset = nanpCodeOffset;
600    }
601  }
602}
603