summaryrefslogtreecommitdiffstats
path: root/src/com/android/dialer/dialpad/SmartDialPrefix.java
blob: 6f993e4c1f86b2283ed8e99d0d3874f8109c243c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.dialer.dialpad;

import android.content.Context;

import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;

/**
 * Smart Dial utility class to find prefixes of contacts. It contains both methods to find supported
 * prefix combinations for contact names, and also methods to find supported prefix combinations for
 * contacts' phone numbers. Each contact name is separated into several tokens, such as first name,
 * middle name, family name etc. Each phone number is also separated into country code, NANP area
 * code, and local number if such separation is possible.
 */
public class SmartDialPrefix {

    /** The number of starting and ending tokens in a contact's name considered for initials.
     * For example, if both constants are set to 2, and a contact's name is
     * "Albert Ben Charles Daniel Ed Foster", the first two tokens "Albert" "Ben", and last two
     * tokens "Ed" "Foster" can be replaced by their initials in contact name matching.
     * Users can look up this contact by combinations of his initials such as "AF" "BF" "EF" "ABF"
     * "BEF" "ABEF" etc, but can not use combinations such as "CF" "DF" "ACF" "ADF" etc.
     */
    private static final int LAST_TOKENS_FOR_INITIALS = 2;
    private static final int FIRST_TOKENS_FOR_INITIALS = 2;

    /** The country code of the user's sim card obtained by calling getSimCountryIso*/
    private static final String PREF_USER_SIM_COUNTRY_CODE =
            "DialtactsActivity_user_sim_country_code";
    private static final String PREF_USER_SIM_COUNTRY_CODE_DEFAULT = null;
    private static String sUserSimCountryCode = PREF_USER_SIM_COUNTRY_CODE_DEFAULT;

    /** Indicates whether user is in NANP regions.*/
    private static boolean sUserInNanpRegion = false;

    /** Set of country names that use NANP code.*/
    private static Set<String> sNanpCountries = null;

    /** Set of supported country codes in front of the phone number. */
    private static Set<String> sCountryCodes = null;

    /** Dialpad mapping. */
    private static SmartDialMap mMap = null;

    private static boolean sNanpInitialized = false;

    /** Initializes the Nanp settings, and finds out whether user is in a NANP region.*/
    public static void initializeNanpSettings(Context context){
        final TelephonyManager manager = (TelephonyManager) context.getSystemService(
                Context.TELEPHONY_SERVICE);
        if (manager != null) {
            sUserSimCountryCode = manager.getSimCountryIso();
        }

        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);

        if (sUserSimCountryCode != null) {
            /** Updates shared preferences with the latest country obtained from getSimCountryIso.*/
            prefs.edit().putString(PREF_USER_SIM_COUNTRY_CODE, sUserSimCountryCode).apply();
        } else {
            /** Uses previously stored country code if loading fails. */
            sUserSimCountryCode = prefs.getString(PREF_USER_SIM_COUNTRY_CODE,
                    PREF_USER_SIM_COUNTRY_CODE_DEFAULT);
        }
        /** Queries the NANP country list to find out whether user is in a NANP region.*/
        sUserInNanpRegion = isCountryNanp(sUserSimCountryCode);

        /** Sets a layout for SmartDial depending on current UI language.*/
        String locale = context.getResources().getConfiguration().locale.getCountry();
        if (locale.equals("RU")) {
            mMap = new RussianSmartDialMap();
        } else if (locale.equals("GR")) {
            mMap = new GreekSmartDialMap();
        } else if (locale.equals("IL")) {
            mMap = new HebrewSmartDialMap();
        } else if (locale.equals("KR")) {
            mMap = new KoreanSmartDialMap();
        } else {
            mMap = new LatinSmartDialMap();
        }

        sNanpInitialized = true;
    }

    /**
     * Explicitly setting the user Nanp to the given boolean
     */
    @VisibleForTesting
    public static void setUserInNanpRegion(boolean userInNanpRegion) {
        sUserInNanpRegion = userInNanpRegion;
    }

    /**
     * Class to record phone number parsing information.
     */
    public static class PhoneNumberTokens {
        /** Country code of the phone number. */
        final String countryCode;

        /** Offset of national number after the country code. */
        final int countryCodeOffset;

        /** Offset of local number after NANP area code.*/
        final int nanpCodeOffset;

        public PhoneNumberTokens(String countryCode, int countryCodeOffset, int nanpCodeOffset) {
            this.countryCode = countryCode;
            this.countryCodeOffset = countryCodeOffset;
            this.nanpCodeOffset = nanpCodeOffset;
        }
    }

    /**
     * Parses a contact's name into a list of separated tokens.
     *
     * @param contactName Contact's name stored in string.
     * @return A list of name tokens, for example separated first names, last name, etc.
     */
    public static ArrayList<String> parseToIndexTokens(String contactName) {
        final int length = contactName.length();
        final ArrayList<String> result = Lists.newArrayList();
        char c;
        final StringBuilder currentIndexToken = new StringBuilder();
        /**
         * Iterates through the whole name string. If the current character is a valid character,
         * append it to the current token. If the current character is not a valid character, for
         * example space " ", mark the current token as complete and add it to the list of tokens.
         */
        for (int i = 0; i < length; i++) {
            c = mMap.normalizeCharacter(contactName.charAt(i));
            if (mMap.isValidDialpadCharacter(c)) {
                /** Converts a character into the number on dialpad that represents the character.*/
                currentIndexToken.append(mMap.getDialpadIndex(c));
            } else {
                if (currentIndexToken.length() != 0) {
                    result.add(currentIndexToken.toString());
                }
                currentIndexToken.delete(0, currentIndexToken.length());
            }
        }

        /** Adds the last token in case it has not been added.*/
        if (currentIndexToken.length() != 0) {
            result.add(currentIndexToken.toString());
        }
        return result;
    }

    /**
     * Generates a list of strings that any prefix of any string in the list can be used to look
     * up the contact's name.
     *
     * @param index The contact's name in string.
     * @return A List of strings, whose prefix can be used to look up the contact.
     */
    public static ArrayList<String> generateNamePrefixes(String index) {
        final ArrayList<String> result = Lists.newArrayList();

        /** Parses the name into a list of tokens.*/
        final ArrayList<String> indexTokens = parseToIndexTokens(index);

        if (indexTokens.size() > 0) {
            /** Adds the full token combinations to the list. For example, a contact with name
             * "Albert Ben Ed Foster" can be looked up by any prefix of the following strings
             * "Foster" "EdFoster" "BenEdFoster" and "AlbertBenEdFoster". This covers all cases of
             * look up that contains only one token, and that spans multiple continuous tokens.
             */
            final StringBuilder fullNameToken = new StringBuilder();
            for (int i = indexTokens.size() - 1; i >= 0; i--) {
                fullNameToken.insert(0, indexTokens.get(i));
                result.add(fullNameToken.toString());
            }

            /** Adds initial combinations to the list, with the number of initials restricted by
             * {@link #LAST_TOKENS_FOR_INITIALS} and {@link #FIRST_TOKENS_FOR_INITIALS}.
             * For example, a contact with name "Albert Ben Ed Foster" can be looked up by any
             * prefix of the following strings "EFoster" "BFoster" "BEFoster" "AFoster" "ABFoster"
             * "AEFoster" and "ABEFoster". This covers all cases of initial lookup.
             */
            ArrayList<String> fullNames = Lists.newArrayList();
            fullNames.add(indexTokens.get(indexTokens.size() - 1));
            final int recursiveNameStart = result.size();
            int recursiveNameEnd = result.size();
            String initial = "";
            for (int i = indexTokens.size() - 2; i >= 0; i--) {
                if ((i >= indexTokens.size() - LAST_TOKENS_FOR_INITIALS) ||
                        (i < FIRST_TOKENS_FOR_INITIALS)) {
                    initial = indexTokens.get(i).substring(0, 1);

                    /** Recursively adds initial combinations to the list.*/
                    for (int j = 0; j < fullNames.size(); ++j) {
                        result.add(initial + fullNames.get(j));
                    }
                    for (int j = recursiveNameStart; j < recursiveNameEnd; ++j) {
                       result.add(initial + result.get(j));
                    }
                    recursiveNameEnd = result.size();
                    final String currentFullName = fullNames.get(fullNames.size() - 1);
                    fullNames.add(indexTokens.get(i) +  currentFullName);
                }
            }
        }

        return result;
    }

    /**
     * Computes a list of number strings based on tokens of a given phone number. Any prefix
     * of any string in the list can be used to look up the phone number. The list include the
     * full phone number, the national number if there is a country code in the phone number, and
     * the local number if there is an area code in the phone number following the NANP format.
     * For example, if a user has phone number +41 71 394 8392, the list will contain 41713948392
     * and 713948392. Any prefix to either of the strings can be used to look up the phone number.
     * If a user has a phone number +1 555-302-3029 (NANP format), the list will contain
     * 15553023029, 5553023029, and 3023029.
     *
     * @param number String of user's phone number.
     * @return A list of strings where any prefix of any entry can be used to look up the number.
     */
    public static ArrayList<String> parseToNumberTokens(String number) {
        final ArrayList<String> result = Lists.newArrayList();
        if (!TextUtils.isEmpty(number)) {
            /** Adds the full number to the list.*/
            result.add(SmartDialNameMatcher.normalizeNumber(number, mMap));

            final PhoneNumberTokens phoneNumberTokens = parsePhoneNumber(number);
            if (phoneNumberTokens == null) {
                return result;
            }

            if (phoneNumberTokens.countryCodeOffset != 0) {
                result.add(SmartDialNameMatcher.normalizeNumber(number,
                        phoneNumberTokens.countryCodeOffset, mMap));
            }

            if (phoneNumberTokens.nanpCodeOffset != 0) {
                result.add(SmartDialNameMatcher.normalizeNumber(number,
                        phoneNumberTokens.nanpCodeOffset, mMap));
            }
        }
        return result;
    }

    /**
     * Parses a phone number to find out whether it has country code and NANP area code.
     *
     * @param number Raw phone number.
     * @return a PhoneNumberToken instance with country code, NANP code information.
     */
    public static PhoneNumberTokens parsePhoneNumber(String number) {
        String countryCode = "";
        int countryCodeOffset = 0;
        int nanpNumberOffset = 0;

        if (!TextUtils.isEmpty(number)) {
            String normalizedNumber = SmartDialNameMatcher.normalizeNumber(number, mMap);
            if (number.charAt(0) == '+') {
                /** If the number starts with '+', tries to find valid country code. */
                for (int i = 1; i <= 1 + 3; i++) {
                    if (number.length() <= i) {
                        break;
                    }
                    countryCode = number.substring(1, i);
                    if (isValidCountryCode(countryCode)) {
                        countryCodeOffset = i;
                        break;
                    }
                }
            } else {
                /** If the number does not start with '+', finds out whether it is in NANP
                 * format and has '1' preceding the number.
                 */
                if ((normalizedNumber.length() == 11) && (normalizedNumber.charAt(0) == '1'
                     || normalizedNumber.charAt(0) == '7') && (sUserInNanpRegion)) {
                    countryCode = normalizedNumber.substring(0, 1);
                    countryCodeOffset = number.indexOf(normalizedNumber.charAt(1));
                    if (countryCodeOffset == -1) {
                        countryCodeOffset = 0;
                    }
                }
            }

            /** If user is in NANP region, finds out whether a number is in NANP format.*/
            if (sUserInNanpRegion)  {
                String areaCode = "";
                if (countryCode.equals("") && normalizedNumber.length() == 10){
                    /** if the number has no country code but fits the NANP format, extracts the
                     * NANP area code, and finds out offset of the local number.
                     */
                    areaCode = normalizedNumber.substring(0, 3);
                } else if ((countryCode.equals("1") || countryCode.equals("7")) &&
                            normalizedNumber.length() == 11) {
                    /** If the number has country code '1', finds out area code and offset of the
                     * local number.
                     */
                    areaCode = normalizedNumber.substring(1, 4);
                }
                if (!areaCode.equals("")) {
                    final int areaCodeIndex = number.indexOf(areaCode);
                    if (areaCodeIndex != -1) {
                        nanpNumberOffset = number.indexOf(areaCode) + 3;
                    }
                }
            }
        }
        return new PhoneNumberTokens(countryCode, countryCodeOffset, nanpNumberOffset);
    }

    /**
     * Checkes whether a country code is valid.
     */
    private static boolean isValidCountryCode(String countryCode) {
        if (sCountryCodes == null) {
            sCountryCodes = initCountryCodes();
        }
        return sCountryCodes.contains(countryCode);
    }

    private static Set<String> initCountryCodes() {
        final HashSet<String> result = new HashSet<String>();
        result.add("1");
        result.add("7");
        result.add("20");
        result.add("27");
        result.add("30");
        result.add("31");
        result.add("32");
        result.add("33");
        result.add("34");
        result.add("36");
        result.add("39");
        result.add("40");
        result.add("41");
        result.add("43");
        result.add("44");
        result.add("45");
        result.add("46");
        result.add("47");
        result.add("48");
        result.add("49");
        result.add("51");
        result.add("52");
        result.add("53");
        result.add("54");
        result.add("55");
        result.add("56");
        result.add("57");
        result.add("58");
        result.add("60");
        result.add("61");
        result.add("62");
        result.add("63");
        result.add("64");
        result.add("65");
        result.add("66");
        result.add("81");
        result.add("82");
        result.add("84");
        result.add("86");
        result.add("90");
        result.add("91");
        result.add("92");
        result.add("93");
        result.add("94");
        result.add("95");
        result.add("98");
        result.add("211");
        result.add("212");
        result.add("213");
        result.add("216");
        result.add("218");
        result.add("220");
        result.add("221");
        result.add("222");
        result.add("223");
        result.add("224");
        result.add("225");
        result.add("226");
        result.add("227");
        result.add("228");
        result.add("229");
        result.add("230");
        result.add("231");
        result.add("232");
        result.add("233");
        result.add("234");
        result.add("235");
        result.add("236");
        result.add("237");
        result.add("238");
        result.add("239");
        result.add("240");
        result.add("241");
        result.add("242");
        result.add("243");
        result.add("244");
        result.add("245");
        result.add("246");
        result.add("247");
        result.add("248");
        result.add("249");
        result.add("250");
        result.add("251");
        result.add("252");
        result.add("253");
        result.add("254");
        result.add("255");
        result.add("256");
        result.add("257");
        result.add("258");
        result.add("260");
        result.add("261");
        result.add("262");
        result.add("263");
        result.add("264");
        result.add("265");
        result.add("266");
        result.add("267");
        result.add("268");
        result.add("269");
        result.add("290");
        result.add("291");
        result.add("297");
        result.add("298");
        result.add("299");
        result.add("350");
        result.add("351");
        result.add("352");
        result.add("353");
        result.add("354");
        result.add("355");
        result.add("356");
        result.add("357");
        result.add("358");
        result.add("359");
        result.add("370");
        result.add("371");
        result.add("372");
        result.add("373");
        result.add("374");
        result.add("375");
        result.add("376");
        result.add("377");
        result.add("378");
        result.add("379");
        result.add("380");
        result.add("381");
        result.add("382");
        result.add("385");
        result.add("386");
        result.add("387");
        result.add("389");
        result.add("420");
        result.add("421");
        result.add("423");
        result.add("500");
        result.add("501");
        result.add("502");
        result.add("503");
        result.add("504");
        result.add("505");
        result.add("506");
        result.add("507");
        result.add("508");
        result.add("509");
        result.add("590");
        result.add("591");
        result.add("592");
        result.add("593");
        result.add("594");
        result.add("595");
        result.add("596");
        result.add("597");
        result.add("598");
        result.add("599");
        result.add("670");
        result.add("672");
        result.add("673");
        result.add("674");
        result.add("675");
        result.add("676");
        result.add("677");
        result.add("678");
        result.add("679");
        result.add("680");
        result.add("681");
        result.add("682");
        result.add("683");
        result.add("685");
        result.add("686");
        result.add("687");
        result.add("688");
        result.add("689");
        result.add("690");
        result.add("691");
        result.add("692");
        result.add("800");
        result.add("808");
        result.add("850");
        result.add("852");
        result.add("853");
        result.add("855");
        result.add("856");
        result.add("870");
        result.add("878");
        result.add("880");
        result.add("881");
        result.add("882");
        result.add("883");
        result.add("886");
        result.add("888");
        result.add("960");
        result.add("961");
        result.add("962");
        result.add("963");
        result.add("964");
        result.add("965");
        result.add("966");
        result.add("967");
        result.add("968");
        result.add("970");
        result.add("971");
        result.add("972");
        result.add("973");
        result.add("974");
        result.add("975");
        result.add("976");
        result.add("977");
        result.add("979");
        result.add("992");
        result.add("993");
        result.add("994");
        result.add("995");
        result.add("996");
        result.add("998");
        return result;
    }

    public static SmartDialMap getMap() {
        return mMap;
    }

    /**
     * Indicates whether the given country uses NANP numbers
     * @see <a href="https://en.wikipedia.org/wiki/North_American_Numbering_Plan">
     *     https://en.wikipedia.org/wiki/North_American_Numbering_Plan</a>
     *
     * @param country ISO 3166 country code (case doesn't matter)
     * @return True if country uses NANP numbers (e.g. US, Canada), false otherwise
     */
    @VisibleForTesting
    public static boolean isCountryNanp(String country) {
        if (TextUtils.isEmpty(country)) {
            return false;
        }
        if (sNanpCountries == null) {
            sNanpCountries = initNanpCountries();
        }
        return sNanpCountries.contains(country.toUpperCase());
    }

    private static Set<String> initNanpCountries() {
        final HashSet<String> result = new HashSet<String>();
        result.add("US"); // United States
        result.add("CA"); // Canada
        result.add("AS"); // American Samoa
        result.add("AI"); // Anguilla
        result.add("AG"); // Antigua and Barbuda
        result.add("BS"); // Bahamas
        result.add("BB"); // Barbados
        result.add("BM"); // Bermuda
        result.add("VG"); // British Virgin Islands
        result.add("KY"); // Cayman Islands
        result.add("DM"); // Dominica
        result.add("DO"); // Dominican Republic
        result.add("GD"); // Grenada
        result.add("GU"); // Guam
        result.add("JM"); // Jamaica
        result.add("PR"); // Puerto Rico
        result.add("MS"); // Montserrat
        result.add("MP"); // Northern Mariana Islands
        result.add("KN"); // Saint Kitts and Nevis
        result.add("LC"); // Saint Lucia
        result.add("VC"); // Saint Vincent and the Grenadines
        result.add("TT"); // Trinidad and Tobago
        result.add("TC"); // Turks and Caicos Islands
        result.add("VI"); // U.S. Virgin Islands
        result.add("RU"); // Russia
        return result;
    }

    /**
     * Returns whether the user is in a region that uses Nanp format based on the sim location.
     *
     * @return Whether user is in Nanp region.
     */
    public static boolean getUserInNanpRegion() {
        return sUserInNanpRegion;
    }
}