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