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