• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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.server.appsearch.contactsindexer;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.res.Resources;
22 import android.provider.ContactsContract.CommonDataKinds.Phone;
23 import android.telephony.PhoneNumberUtils;
24 import android.text.TextUtils;
25 import android.util.ArraySet;
26 import android.util.Log;
27 import android.util.Pair;
28 
29 import com.android.internal.annotations.VisibleForTesting;
30 
31 import java.util.Objects;
32 import java.util.Set;
33 
34 /**
35  * Class to provide utilities to handle phone numbers.
36  *
37  * @hide
38  */
39 public class ContactsIndexerPhoneNumberUtils {
40     // 3 digits international calling code and the leading "+". E.g. "+354" for Iceland.
41     // So maximum 4 characters total.
42     @VisibleForTesting
43     static final int DIALING_CODE_WITH_PLUS_SIGN_MAX_DIGITS = 4;
44     private static final String TAG = "ContactsIndexerPhoneNumberUtils";
45 
46     /**
47      * Creates different phone number variants for the given phone number.
48      *
49      * <p>The different formats we will try are the normalized format, national format and its
50      * variants, and the E164 representation.
51      *
52      * <p>The locales on the current system configurations will be used to determine the e164
53      * representation and the country code used for national format.
54      *
55      * <p>This method is doing best effort to generate those variants, which are nice to have.
56      * Depending on the format original phone number is using, and the locales on the system, it may
57      * not be able to produce all the variants.
58      *
59      * @param resources           the application's resource
60      * @param phoneNumberOriginal the phone number in the original form from CP2.
61      * @param phoneNumberFromCP2InE164 the phone number in e164 from {@link Phone#NORMALIZED_NUMBER}
62      * @return a set containing different phone variants created.
63      */
64     @NonNull
createPhoneNumberVariants(@onNull Resources resources, @NonNull String phoneNumberOriginal, @Nullable String phoneNumberFromCP2InE164)65     public static Set<String> createPhoneNumberVariants(@NonNull Resources resources,
66             @NonNull String phoneNumberOriginal, @Nullable String phoneNumberFromCP2InE164) {
67         Objects.requireNonNull(resources);
68         Objects.requireNonNull(phoneNumberOriginal);
69 
70         Set<String> phoneNumberVariants = new ArraySet<>();
71         try {
72             // Normalize the phone number. It may or may not include country code, depending on
73             // the original phone number.
74             // With country code: "1 (202) 555-0111" -> "12025550111"
75             //                     "+1 (202) 555-0111" -> "+12025550111"
76             // Without country code: "(202) 555-0111" -> "2025550111"
77             String phoneNumberNormalized = PhoneNumberUtils.normalizeNumber(phoneNumberOriginal);
78             if (TextUtils.isEmpty(phoneNumberNormalized)) {
79                 return phoneNumberVariants;
80             }
81             phoneNumberVariants.add(phoneNumberNormalized);
82 
83             String phoneNumberInE164 = phoneNumberFromCP2InE164;
84             if (TextUtils.isEmpty(phoneNumberInE164)) {
85                 if (!phoneNumberNormalized.startsWith("+")) {
86                     // e164 format is not provided by CP2 and the normalized phone number isn't
87                     // in e164 either. Nothing more can be done. Just return.
88                     return phoneNumberVariants;
89                 }
90                 // e164 form is not provided by CP2, but the original phone number is likely
91                 // to be in e164.
92                 phoneNumberInE164 = phoneNumberNormalized;
93             }
94             phoneNumberInE164 = PhoneNumberUtils.normalizeNumber(phoneNumberInE164);
95             phoneNumberVariants.add(phoneNumberInE164);
96 
97             // E.g. "+12025550111" will be split into dialingCode "+1" and phoneNumberNormalized
98             // without country code: "2025550111".
99             Pair<String, String> result = parsePhoneNumberInE164(phoneNumberInE164);
100             if (result == null) {
101                 return phoneNumberVariants;
102             }
103             String dialingCode = result.first;
104             // "+1" -> "US"
105             String isoCountryCode = CountryCodeUtils.COUNTRY_TO_REGIONAL_CODE.get(dialingCode);
106             if (TextUtils.isEmpty(isoCountryCode)) {
107                 return phoneNumberVariants;
108             }
109             String phoneNumberNormalizedWithoutCountryCode = result.second;
110             phoneNumberVariants.add(phoneNumberNormalizedWithoutCountryCode);
111             // create phone number in national format, and generate variants based on it.
112             String nationalFormat = createFormatNational(phoneNumberNormalizedWithoutCountryCode,
113                     isoCountryCode);
114             // lastly, we want to index a national format with a country dialing code:
115             // E.g. for (202) 555-0111, we also want to index "1 (202) 555-0111". So when the query
116             // is "1 202" or "1 (202)", a match can still be returned.
117             if (TextUtils.isEmpty(nationalFormat)) {
118                 return phoneNumberVariants;
119             }
120             addVariantsFromFormatNational(nationalFormat, phoneNumberVariants);
121 
122             // Put dialing code without "+" at the front of the national format(e.g. (202)
123             // 555-0111) so we can index something like "1 (202) 555-0111". With this, we can
124             // support more search queries starting with the international dialing code.
125             phoneNumberVariants.add(dialingCode.substring(1) + " " + nationalFormat);
126         } catch (Throwable t) {
127             Log.w(TAG, "Exception thrown while creating phone variants.", t);
128         }
129         return phoneNumberVariants;
130     }
131 
132     /**
133      * Parses a phone number in e164 format.
134      *
135      * @return a pair of dialing code and a normalized phone number without the dialing code. E.g.
136      * for +12025550111, this function returns "+1" and "2025550111". {@code null} if phone number
137      * is not in a valid e164 form.
138      */
139     @Nullable
parsePhoneNumberInE164(@onNull String phoneNumberInE164)140     static Pair<String, String> parsePhoneNumberInE164(@NonNull String phoneNumberInE164) {
141         Objects.requireNonNull(phoneNumberInE164);
142 
143         if (!phoneNumberInE164.startsWith("+")) {
144             return null;
145         }
146         // For e164, the calling code has maximum 3 digits, and it should start with '+' like
147         // "+12025550111".
148         int len = Math.min(DIALING_CODE_WITH_PLUS_SIGN_MAX_DIGITS, phoneNumberInE164.length());
149         for (int i = 2; i <= len; ++i) {
150             String possibleCodeWithPlusSign = phoneNumberInE164.substring(0, i);
151             if (CountryCodeUtils.COUNTRY_DIALING_CODE.contains(possibleCodeWithPlusSign)) {
152                 return new Pair<>(possibleCodeWithPlusSign, phoneNumberInE164.substring(i));
153             }
154         }
155 
156         return null;
157     }
158 
159     /**
160      * Creates a national phone format based on a normalized phone number.
161      *
162      * <p>For a normalized phone number 2025550111, the national format will be (202) 555-0111 with
163      * country code "US".
164      *
165      * @param phoneNumberNormalized normalized number. E.g. for phone number 202-555-0111, its
166      *                              normalized form would be 2025550111.
167      * @param countryCode           the country code to be used to format the phone number. If it is
168      *                              {@code null}, it will try the country codes from the locales in
169      *                              the configuration and return the first match.
170      * @return the national format of the phone number. {@code null} if {@code countryCode} is
171      * {@code null}.
172      */
173     @Nullable
createFormatNational(@onNull String phoneNumberNormalized, @Nullable String countryCode)174     static String createFormatNational(@NonNull String phoneNumberNormalized,
175             @Nullable String countryCode) {
176         Objects.requireNonNull(phoneNumberNormalized);
177 
178         if (TextUtils.isEmpty(countryCode)) {
179             return null;
180         }
181         return PhoneNumberUtils.formatNumber(phoneNumberNormalized, countryCode);
182     }
183 
184     /**
185      * Adds the variants generated from the phone number in national format into the given
186      * set.
187      *
188      * <p>E.g. for national format (202) 555-0111, we will add itself as a variant, as well as (202)
189      * 5550111 by removing the hyphen(last non-digit character).
190      *
191      * @param phoneNumberNational phone number in national format. E.g. (202)-555-0111
192      * @param phoneNumberVariants set to hold the generated variants.
193      */
addVariantsFromFormatNational(@ullable String phoneNumberNational, @NonNull Set<String> phoneNumberVariants)194     static void addVariantsFromFormatNational(@Nullable String phoneNumberNational,
195             @NonNull Set<String> phoneNumberVariants) {
196         Objects.requireNonNull(phoneNumberVariants);
197 
198         if (TextUtils.isEmpty(phoneNumberNational)) {
199             return;
200         }
201         phoneNumberVariants.add(phoneNumberNational);
202         // Remove the last non-digit character from the national format. So "(202) 555-0111"
203         // becomes "(202) 5550111". And query "5550" can return the expected result.
204         int i;
205         for (i = phoneNumberNational.length() - 1; i >= 0; --i) {
206             char c = phoneNumberNational.charAt(i);
207             // last non-digit character in the national format.
208             if (c < '0' || c > '9') {
209                 break;
210             }
211         }
212         if (i >= 0) {
213             phoneNumberVariants.add(
214                     phoneNumberNational.substring(0, i) + phoneNumberNational.substring(i + 1));
215         }
216     }
217 }
218