/* * Copyright (C) 2022 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.server.appsearch.contactsindexer; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.res.Resources; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.telephony.PhoneNumberUtils; import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; import android.util.Pair; import com.android.internal.annotations.VisibleForTesting; import java.util.Objects; import java.util.Set; /** * Class to provide utilities to handle phone numbers. * * @hide */ public class ContactsIndexerPhoneNumberUtils { // 3 digits international calling code and the leading "+". E.g. "+354" for Iceland. // So maximum 4 characters total. @VisibleForTesting static final int DIALING_CODE_WITH_PLUS_SIGN_MAX_DIGITS = 4; private static final String TAG = "ContactsIndexerPhoneNumberUtils"; /** * Creates different phone number variants for the given phone number. * *

The different formats we will try are the normalized format, national format and its * variants, and the E164 representation. * *

The locales on the current system configurations will be used to determine the e164 * representation and the country code used for national format. * *

This method is doing best effort to generate those variants, which are nice to have. * Depending on the format original phone number is using, and the locales on the system, it may * not be able to produce all the variants. * * @param resources the application's resource * @param phoneNumberOriginal the phone number in the original form from CP2. * @param phoneNumberFromCP2InE164 the phone number in e164 from {@link Phone#NORMALIZED_NUMBER} * @return a set containing different phone variants created. */ @NonNull public static Set createPhoneNumberVariants( @NonNull Resources resources, @NonNull String phoneNumberOriginal, @Nullable String phoneNumberFromCP2InE164) { Objects.requireNonNull(resources); Objects.requireNonNull(phoneNumberOriginal); Set phoneNumberVariants = new ArraySet<>(); try { // Normalize the phone number. It may or may not include country code, depending on // the original phone number. // With country code: "1 (202) 555-0111" -> "12025550111" // "+1 (202) 555-0111" -> "+12025550111" // Without country code: "(202) 555-0111" -> "2025550111" String phoneNumberNormalized = PhoneNumberUtils.normalizeNumber(phoneNumberOriginal); if (TextUtils.isEmpty(phoneNumberNormalized)) { return phoneNumberVariants; } phoneNumberVariants.add(phoneNumberNormalized); String phoneNumberInE164 = phoneNumberFromCP2InE164; if (TextUtils.isEmpty(phoneNumberInE164)) { if (!phoneNumberNormalized.startsWith("+")) { // e164 format is not provided by CP2 and the normalized phone number isn't // in e164 either. Nothing more can be done. Just return. return phoneNumberVariants; } // e164 form is not provided by CP2, but the original phone number is likely // to be in e164. phoneNumberInE164 = phoneNumberNormalized; } phoneNumberInE164 = PhoneNumberUtils.normalizeNumber(phoneNumberInE164); phoneNumberVariants.add(phoneNumberInE164); // E.g. "+12025550111" will be split into dialingCode "+1" and phoneNumberNormalized // without country code: "2025550111". Pair result = parsePhoneNumberInE164(phoneNumberInE164); if (result == null) { return phoneNumberVariants; } String dialingCode = result.first; // "+1" -> "US" String isoCountryCode = CountryCodeUtils.COUNTRY_TO_REGIONAL_CODE.get(dialingCode); if (TextUtils.isEmpty(isoCountryCode)) { return phoneNumberVariants; } String phoneNumberNormalizedWithoutCountryCode = result.second; phoneNumberVariants.add(phoneNumberNormalizedWithoutCountryCode); // create phone number in national format, and generate variants based on it. String nationalFormat = createFormatNational(phoneNumberNormalizedWithoutCountryCode, isoCountryCode); // lastly, we want to index a national format with a country dialing code: // E.g. for (202) 555-0111, we also want to index "1 (202) 555-0111". So when the query // is "1 202" or "1 (202)", a match can still be returned. if (TextUtils.isEmpty(nationalFormat)) { return phoneNumberVariants; } addVariantsFromFormatNational(nationalFormat, phoneNumberVariants); // Put dialing code without "+" at the front of the national format(e.g. (202) // 555-0111) so we can index something like "1 (202) 555-0111". With this, we can // support more search queries starting with the international dialing code. phoneNumberVariants.add(dialingCode.substring(1) + " " + nationalFormat); } catch (Throwable t) { Log.w(TAG, "Exception thrown while creating phone variants.", t); } return phoneNumberVariants; } /** * Parses a phone number in e164 format. * * @return a pair of dialing code and a normalized phone number without the dialing code. E.g. * for +12025550111, this function returns "+1" and "2025550111". {@code null} if phone * number is not in a valid e164 form. */ @Nullable static Pair parsePhoneNumberInE164(@NonNull String phoneNumberInE164) { Objects.requireNonNull(phoneNumberInE164); if (!phoneNumberInE164.startsWith("+")) { return null; } // For e164, the calling code has maximum 3 digits, and it should start with '+' like // "+12025550111". int len = Math.min(DIALING_CODE_WITH_PLUS_SIGN_MAX_DIGITS, phoneNumberInE164.length()); for (int i = 2; i <= len; ++i) { String possibleCodeWithPlusSign = phoneNumberInE164.substring(0, i); if (CountryCodeUtils.COUNTRY_DIALING_CODE.contains(possibleCodeWithPlusSign)) { return new Pair<>(possibleCodeWithPlusSign, phoneNumberInE164.substring(i)); } } return null; } /** * Creates a national phone format based on a normalized phone number. * *

For a normalized phone number 2025550111, the national format will be (202) 555-0111 with * country code "US". * * @param phoneNumberNormalized normalized number. E.g. for phone number 202-555-0111, its * normalized form would be 2025550111. * @param countryCode the country code to be used to format the phone number. If it is {@code * null}, it will try the country codes from the locales in the configuration and return the * first match. * @return the national format of the phone number. {@code null} if {@code countryCode} is * {@code null}. */ @Nullable static String createFormatNational( @NonNull String phoneNumberNormalized, @Nullable String countryCode) { Objects.requireNonNull(phoneNumberNormalized); if (TextUtils.isEmpty(countryCode)) { return null; } return PhoneNumberUtils.formatNumber(phoneNumberNormalized, countryCode); } /** * Adds the variants generated from the phone number in national format into the given set. * *

E.g. for national format (202) 555-0111, we will add itself as a variant, as well as (202) * 5550111 by removing the hyphen(last non-digit character). * * @param phoneNumberNational phone number in national format. E.g. (202)-555-0111 * @param phoneNumberVariants set to hold the generated variants. */ static void addVariantsFromFormatNational( @Nullable String phoneNumberNational, @NonNull Set phoneNumberVariants) { Objects.requireNonNull(phoneNumberVariants); if (TextUtils.isEmpty(phoneNumberNational)) { return; } phoneNumberVariants.add(phoneNumberNational); // Remove the last non-digit character from the national format. So "(202) 555-0111" // becomes "(202) 5550111". And query "5550" can return the expected result. int i; for (i = phoneNumberNational.length() - 1; i >= 0; --i) { char c = phoneNumberNational.charAt(i); // last non-digit character in the national format. if (c < '0' || c > '9') { break; } } if (i >= 0) { phoneNumberVariants.add( phoneNumberNational.substring(0, i) + phoneNumberNational.substring(i + 1)); } } }