/*
* 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));
}
}
}