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.dialpad;
18
19import android.content.Context;
20
21import android.content.SharedPreferences;
22import android.preference.PreferenceManager;
23import android.telephony.TelephonyManager;
24import android.text.TextUtils;
25
26import com.google.common.annotations.VisibleForTesting;
27import com.google.common.collect.Lists;
28
29import java.util.ArrayList;
30import java.util.HashSet;
31import 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 */
40public 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.*/
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
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
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     */
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     */
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     */
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     */
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     */
321    private static boolean isValidCountryCode(String countryCode) {
322        if (sCountryCodes == null) {
323            sCountryCodes = initCountryCodes();
324        }
325        return sCountryCodes.contains(countryCode);
326    }
327
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
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
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
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     */
605    public static boolean getUserInNanpRegion() {
606        return sUserInNanpRegion;
607    }
608}
609