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 static final int DIALING_CODE_WITH_PLUS_SIGN_MAX_DIGITS = 4;
43     private static final String TAG = "ContactsIndexerPhoneNumberUtils";
44 
45     /**
46      * Creates different phone number variants for the given phone number.
47      *
48      * <p>The different formats we will try are the normalized format, national format and its
49      * variants, and the E164 representation.
50      *
51      * <p>The locales on the current system configurations will be used to determine the e164
52      * representation and the country code used for national format.
53      *
54      * <p>This method is doing best effort to generate those variants, which are nice to have.
55      * Depending on the format original phone number is using, and the locales on the system, it may
56      * not be able to produce all the variants.
57      *
58      * @param resources the application's resource
59      * @param phoneNumberOriginal the phone number in the original form from CP2.
60      * @param phoneNumberFromCP2InE164 the phone number in e164 from {@link Phone#NORMALIZED_NUMBER}
61      * @return a set containing different phone variants created.
62      */
63     @NonNull
createPhoneNumberVariants( @onNull Resources resources, @NonNull String phoneNumberOriginal, @Nullable String phoneNumberFromCP2InE164)64     public static Set<String> createPhoneNumberVariants(
65             @NonNull Resources resources,
66             @NonNull String phoneNumberOriginal,
67             @Nullable String phoneNumberFromCP2InE164) {
68         Objects.requireNonNull(resources);
69         Objects.requireNonNull(phoneNumberOriginal);
70 
71         Set<String> phoneNumberVariants = new ArraySet<>();
72         try {
73             // Normalize the phone number. It may or may not include country code, depending on
74             // the original phone number.
75             // With country code: "1 (202) 555-0111" -> "12025550111"
76             //                     "+1 (202) 555-0111" -> "+12025550111"
77             // Without country code: "(202) 555-0111" -> "2025550111"
78             String phoneNumberNormalized = PhoneNumberUtils.normalizeNumber(phoneNumberOriginal);
79             if (TextUtils.isEmpty(phoneNumberNormalized)) {
80                 return phoneNumberVariants;
81             }
82             phoneNumberVariants.add(phoneNumberNormalized);
83 
84             String phoneNumberInE164 = phoneNumberFromCP2InE164;
85             if (TextUtils.isEmpty(phoneNumberInE164)) {
86                 if (!phoneNumberNormalized.startsWith("+")) {
87                     // e164 format is not provided by CP2 and the normalized phone number isn't
88                     // in e164 either. Nothing more can be done. Just return.
89                     return phoneNumberVariants;
90                 }
91                 // e164 form is not provided by CP2, but the original phone number is likely
92                 // to be in e164.
93                 phoneNumberInE164 = phoneNumberNormalized;
94             }
95             phoneNumberInE164 = PhoneNumberUtils.normalizeNumber(phoneNumberInE164);
96             phoneNumberVariants.add(phoneNumberInE164);
97 
98             // E.g. "+12025550111" will be split into dialingCode "+1" and phoneNumberNormalized
99             // without country code: "2025550111".
100             Pair<String, String> result = parsePhoneNumberInE164(phoneNumberInE164);
101             if (result == null) {
102                 return phoneNumberVariants;
103             }
104             String dialingCode = result.first;
105             // "+1" -> "US"
106             String isoCountryCode = CountryCodeUtils.COUNTRY_TO_REGIONAL_CODE.get(dialingCode);
107             if (TextUtils.isEmpty(isoCountryCode)) {
108                 return phoneNumberVariants;
109             }
110             String phoneNumberNormalizedWithoutCountryCode = result.second;
111             phoneNumberVariants.add(phoneNumberNormalizedWithoutCountryCode);
112             // create phone number in national format, and generate variants based on it.
113             String nationalFormat =
114                     createFormatNational(phoneNumberNormalizedWithoutCountryCode, isoCountryCode);
115             // lastly, we want to index a national format with a country dialing code:
116             // E.g. for (202) 555-0111, we also want to index "1 (202) 555-0111". So when the query
117             // is "1 202" or "1 (202)", a match can still be returned.
118             if (TextUtils.isEmpty(nationalFormat)) {
119                 return phoneNumberVariants;
120             }
121             addVariantsFromFormatNational(nationalFormat, phoneNumberVariants);
122 
123             // Put dialing code without "+" at the front of the national format(e.g. (202)
124             // 555-0111) so we can index something like "1 (202) 555-0111". With this, we can
125             // support more search queries starting with the international dialing code.
126             phoneNumberVariants.add(dialingCode.substring(1) + " " + nationalFormat);
127         } catch (Throwable t) {
128             Log.w(TAG, "Exception thrown while creating phone variants.", t);
129         }
130         return phoneNumberVariants;
131     }
132 
133     /**
134      * Parses a phone number in e164 format.
135      *
136      * @return a pair of dialing code and a normalized phone number without the dialing code. E.g.
137      *     for +12025550111, this function returns "+1" and "2025550111". {@code null} if phone
138      *     number is not in a valid e164 form.
139      */
140     @Nullable
parsePhoneNumberInE164(@onNull String phoneNumberInE164)141     static Pair<String, String> parsePhoneNumberInE164(@NonNull String phoneNumberInE164) {
142         Objects.requireNonNull(phoneNumberInE164);
143 
144         if (!phoneNumberInE164.startsWith("+")) {
145             return null;
146         }
147         // For e164, the calling code has maximum 3 digits, and it should start with '+' like
148         // "+12025550111".
149         int len = Math.min(DIALING_CODE_WITH_PLUS_SIGN_MAX_DIGITS, phoneNumberInE164.length());
150         for (int i = 2; i <= len; ++i) {
151             String possibleCodeWithPlusSign = phoneNumberInE164.substring(0, i);
152             if (CountryCodeUtils.COUNTRY_DIALING_CODE.contains(possibleCodeWithPlusSign)) {
153                 return new Pair<>(possibleCodeWithPlusSign, phoneNumberInE164.substring(i));
154             }
155         }
156 
157         return null;
158     }
159 
160     /**
161      * Creates a national phone format based on a normalized phone number.
162      *
163      * <p>For a normalized phone number 2025550111, the national format will be (202) 555-0111 with
164      * country code "US".
165      *
166      * @param phoneNumberNormalized normalized number. E.g. for phone number 202-555-0111, its
167      *     normalized form would be 2025550111.
168      * @param countryCode the country code to be used to format the phone number. If it is {@code
169      *     null}, it will try the country codes from the locales in the configuration and return the
170      *     first match.
171      * @return the national format of the phone number. {@code null} if {@code countryCode} is
172      *     {@code null}.
173      */
174     @Nullable
createFormatNational( @onNull String phoneNumberNormalized, @Nullable String countryCode)175     static String createFormatNational(
176             @NonNull String phoneNumberNormalized, @Nullable String countryCode) {
177         Objects.requireNonNull(phoneNumberNormalized);
178 
179         if (TextUtils.isEmpty(countryCode)) {
180             return null;
181         }
182         return PhoneNumberUtils.formatNumber(phoneNumberNormalized, countryCode);
183     }
184 
185     /**
186      * Adds the variants generated from the phone number in national format into the given 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(
195             @Nullable String phoneNumberNational, @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