/* * Copyright (C) 2013 The Libphonenumber Authors * * 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.i18n.phonenumbers; import com.android.i18n.phonenumbers.Phonemetadata.PhoneMetadata; import com.android.i18n.phonenumbers.Phonemetadata.PhoneNumberDesc; import com.android.i18n.phonenumbers.Phonenumber.PhoneNumber; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; /** * Methods for getting information about short phone numbers, such as short codes and emergency * numbers. Note that most commercial short numbers are not handled here, but by the * {@link PhoneNumberUtil}. * * @author Shaopeng Jia * @author David Yonge-Mallo */ public class ShortNumberInfo { private static final Logger logger = Logger.getLogger(ShortNumberInfo.class.getName()); private static final ShortNumberInfo INSTANCE = new ShortNumberInfo(PhoneNumberUtil.getInstance()); // In these countries, if extra digits are added to an emergency number, it no longer connects // to the emergency service. private static final Set REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT = new HashSet(); static { REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT.add("BR"); REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT.add("CL"); REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT.add("NI"); } /** Cost categories of short numbers. */ public enum ShortNumberCost { TOLL_FREE, STANDARD_RATE, PREMIUM_RATE, UNKNOWN_COST } /** Returns the singleton instance of the ShortNumberInfo. */ public static ShortNumberInfo getInstance() { return INSTANCE; } private final PhoneNumberUtil phoneUtil; // @VisibleForTesting ShortNumberInfo(PhoneNumberUtil util) { phoneUtil = util; } /** * Check whether a short number is a possible number when dialled from a region, given the number * in the form of a string, and the region where the number is dialed from. This provides a more * lenient check than {@link #isValidShortNumberForRegion}. * * @param shortNumber the short number to check as a string * @param regionDialingFrom the region from which the number is dialed * @return whether the number is a possible short number */ public boolean isPossibleShortNumberForRegion(String shortNumber, String regionDialingFrom) { PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(regionDialingFrom); if (phoneMetadata == null) { return false; } PhoneNumberDesc generalDesc = phoneMetadata.getGeneralDesc(); return phoneUtil.isNumberPossibleForDesc(shortNumber, generalDesc); } /** * Check whether a short number is a possible number. If a country calling code is shared by * multiple regions, this returns true if it's possible in any of them. This provides a more * lenient check than {@link #isValidShortNumber}. See {@link * #isPossibleShortNumberForRegion(String, String)} for details. * * @param number the short number to check * @return whether the number is a possible short number */ public boolean isPossibleShortNumber(PhoneNumber number) { List regionCodes = phoneUtil.getRegionCodesForCountryCode(number.getCountryCode()); String shortNumber = phoneUtil.getNationalSignificantNumber(number); for (String region : regionCodes) { PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(region); if (phoneUtil.isNumberPossibleForDesc(shortNumber, phoneMetadata.getGeneralDesc())) { return true; } } return false; } /** * Tests whether a short number matches a valid pattern in a region. Note that this doesn't verify * the number is actually in use, which is impossible to tell by just looking at the number * itself. * * @param shortNumber the short number to check as a string * @param regionDialingFrom the region from which the number is dialed * @return whether the short number matches a valid pattern */ public boolean isValidShortNumberForRegion(String shortNumber, String regionDialingFrom) { PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(regionDialingFrom); if (phoneMetadata == null) { return false; } PhoneNumberDesc generalDesc = phoneMetadata.getGeneralDesc(); if (!generalDesc.hasNationalNumberPattern() || !phoneUtil.isNumberMatchingDesc(shortNumber, generalDesc)) { return false; } PhoneNumberDesc shortNumberDesc = phoneMetadata.getShortCode(); if (!shortNumberDesc.hasNationalNumberPattern()) { logger.log(Level.WARNING, "No short code national number pattern found for region: " + regionDialingFrom); return false; } return phoneUtil.isNumberMatchingDesc(shortNumber, shortNumberDesc); } /** * Tests whether a short number matches a valid pattern. If a country calling code is shared by * multiple regions, this returns true if it's valid in any of them. Note that this doesn't verify * the number is actually in use, which is impossible to tell by just looking at the number * itself. See {@link #isValidShortNumberForRegion(String, String)} for details. * * @param number the short number for which we want to test the validity * @return whether the short number matches a valid pattern */ public boolean isValidShortNumber(PhoneNumber number) { List regionCodes = phoneUtil.getRegionCodesForCountryCode(number.getCountryCode()); String shortNumber = phoneUtil.getNationalSignificantNumber(number); String regionCode = getRegionCodeForShortNumberFromRegionList(number, regionCodes); if (regionCodes.size() > 1 && regionCode != null) { // If a matching region had been found for the phone number from among two or more regions, // then we have already implicitly verified its validity for that region. return true; } return isValidShortNumberForRegion(shortNumber, regionCode); } /** * Gets the expected cost category of a short number when dialled from a region (however, nothing * is implied about its validity). If it is important that the number is valid, then its validity * must first be checked using {@link isValidShortNumberForRegion}. Note that emergency numbers * are always considered toll-free. Example usage: *
{@code
   * ShortNumberInfo shortInfo = ShortNumberInfo.getInstance();
   * String shortNumber = "110";
   * String regionCode = "FR";
   * if (shortInfo.isValidShortNumberForRegion(shortNumber, regionCode)) {
   *   ShortNumberInfo.ShortNumberCost cost = shortInfo.getExpectedCostForRegion(shortNumber,
   *       regionCode);
   *   // Do something with the cost information here.
   * }}
* * @param shortNumber the short number for which we want to know the expected cost category, * as a string * @param regionDialingFrom the region from which the number is dialed * @return the expected cost category for that region of the short number. Returns UNKNOWN_COST if * the number does not match a cost category. Note that an invalid number may match any cost * category. */ public ShortNumberCost getExpectedCostForRegion(String shortNumber, String regionDialingFrom) { // Note that regionDialingFrom may be null, in which case phoneMetadata will also be null. PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion( regionDialingFrom); if (phoneMetadata == null) { return ShortNumberCost.UNKNOWN_COST; } // The cost categories are tested in order of decreasing expense, since if for some reason the // patterns overlap the most expensive matching cost category should be returned. if (phoneUtil.isNumberMatchingDesc(shortNumber, phoneMetadata.getPremiumRate())) { return ShortNumberCost.PREMIUM_RATE; } if (phoneUtil.isNumberMatchingDesc(shortNumber, phoneMetadata.getStandardRate())) { return ShortNumberCost.STANDARD_RATE; } if (phoneUtil.isNumberMatchingDesc(shortNumber, phoneMetadata.getTollFree())) { return ShortNumberCost.TOLL_FREE; } if (isEmergencyNumber(shortNumber, regionDialingFrom)) { // Emergency numbers are implicitly toll-free. return ShortNumberCost.TOLL_FREE; } return ShortNumberCost.UNKNOWN_COST; } /** * Gets the expected cost category of a short number (however, nothing is implied about its * validity). If the country calling code is unique to a region, this method behaves exactly the * same as {@link #getExpectedCostForRegion(String, String)}. However, if the country calling * code is shared by multiple regions, then it returns the highest cost in the sequence * PREMIUM_RATE, UNKNOWN_COST, STANDARD_RATE, TOLL_FREE. The reason for the position of * UNKNOWN_COST in this order is that if a number is UNKNOWN_COST in one region but STANDARD_RATE * or TOLL_FREE in another, its expected cost cannot be estimated as one of the latter since it * might be a PREMIUM_RATE number. * * For example, if a number is STANDARD_RATE in the US, but TOLL_FREE in Canada, the expected cost * returned by this method will be STANDARD_RATE, since the NANPA countries share the same country * calling code. * * Note: If the region from which the number is dialed is known, it is highly preferable to call * {@link #getExpectedCostForRegion(String, String)} instead. * * @param number the short number for which we want to know the expected cost category * @return the highest expected cost category of the short number in the region(s) with the given * country calling code */ public ShortNumberCost getExpectedCost(PhoneNumber number) { List regionCodes = phoneUtil.getRegionCodesForCountryCode(number.getCountryCode()); if (regionCodes.size() == 0) { return ShortNumberCost.UNKNOWN_COST; } String shortNumber = phoneUtil.getNationalSignificantNumber(number); if (regionCodes.size() == 1) { return getExpectedCostForRegion(shortNumber, regionCodes.get(0)); } ShortNumberCost cost = ShortNumberCost.TOLL_FREE; for (String regionCode : regionCodes) { ShortNumberCost costForRegion = getExpectedCostForRegion(shortNumber, regionCode); switch (costForRegion) { case PREMIUM_RATE: return ShortNumberCost.PREMIUM_RATE; case UNKNOWN_COST: cost = ShortNumberCost.UNKNOWN_COST; break; case STANDARD_RATE: if (cost != ShortNumberCost.UNKNOWN_COST) { cost = ShortNumberCost.STANDARD_RATE; } break; case TOLL_FREE: // Do nothing. break; default: logger.log(Level.SEVERE, "Unrecognised cost for region: " + costForRegion); } } return cost; } // Helper method to get the region code for a given phone number, from a list of possible region // codes. If the list contains more than one region, the first region for which the number is // valid is returned. private String getRegionCodeForShortNumberFromRegionList(PhoneNumber number, List regionCodes) { if (regionCodes.size() == 0) { return null; } else if (regionCodes.size() == 1) { return regionCodes.get(0); } String nationalNumber = phoneUtil.getNationalSignificantNumber(number); for (String regionCode : regionCodes) { PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(regionCode); if (phoneMetadata != null && phoneUtil.isNumberMatchingDesc(nationalNumber, phoneMetadata.getShortCode())) { // The number is valid for this region. return regionCode; } } return null; } /** * Convenience method to get a list of what regions the library has metadata for. */ Set getSupportedRegions() { return Collections.unmodifiableSet(MetadataManager.getShortNumberMetadataSupportedRegions()); } /** * Gets a valid short number for the specified region. * * @param regionCode the region for which an example short number is needed * @return a valid short number for the specified region. Returns an empty string when the * metadata does not contain such information. */ // @VisibleForTesting String getExampleShortNumber(String regionCode) { PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(regionCode); if (phoneMetadata == null) { return ""; } PhoneNumberDesc desc = phoneMetadata.getShortCode(); if (desc.hasExampleNumber()) { return desc.getExampleNumber(); } return ""; } /** * Gets a valid short number for the specified cost category. * * @param regionCode the region for which an example short number is needed * @param cost the cost category of number that is needed * @return a valid short number for the specified region and cost category. Returns an empty * string when the metadata does not contain such information, or the cost is UNKNOWN_COST. */ // @VisibleForTesting String getExampleShortNumberForCost(String regionCode, ShortNumberCost cost) { PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(regionCode); if (phoneMetadata == null) { return ""; } PhoneNumberDesc desc = null; switch (cost) { case TOLL_FREE: desc = phoneMetadata.getTollFree(); break; case STANDARD_RATE: desc = phoneMetadata.getStandardRate(); break; case PREMIUM_RATE: desc = phoneMetadata.getPremiumRate(); break; default: // UNKNOWN_COST numbers are computed by the process of elimination from the other cost // categories. } if (desc != null && desc.hasExampleNumber()) { return desc.getExampleNumber(); } return ""; } /** * Returns true if the number might be used to connect to an emergency service in the given * region. * * This method takes into account cases where the number might contain formatting, or might have * additional digits appended (when it is okay to do that in the region specified). * * @param number the phone number to test * @param regionCode the region where the phone number is being dialed * @return whether the number might be used to connect to an emergency service in the given region */ public boolean connectsToEmergencyNumber(String number, String regionCode) { return matchesEmergencyNumberHelper(number, regionCode, true /* allows prefix match */); } /** * Returns true if the number exactly matches an emergency service number in the given region. * * This method takes into account cases where the number might contain formatting, but doesn't * allow additional digits to be appended. * * @param number the phone number to test * @param regionCode the region where the phone number is being dialed * @return whether the number exactly matches an emergency services number in the given region */ public boolean isEmergencyNumber(String number, String regionCode) { return matchesEmergencyNumberHelper(number, regionCode, false /* doesn't allow prefix match */); } private boolean matchesEmergencyNumberHelper(String number, String regionCode, boolean allowPrefixMatch) { number = PhoneNumberUtil.extractPossibleNumber(number); if (PhoneNumberUtil.PLUS_CHARS_PATTERN.matcher(number).lookingAt()) { // Returns false if the number starts with a plus sign. We don't believe dialing the country // code before emergency numbers (e.g. +1911) works, but later, if that proves to work, we can // add additional logic here to handle it. return false; } PhoneMetadata metadata = MetadataManager.getShortNumberMetadataForRegion(regionCode); if (metadata == null || !metadata.hasEmergency()) { return false; } Pattern emergencyNumberPattern = Pattern.compile(metadata.getEmergency().getNationalNumberPattern()); String normalizedNumber = PhoneNumberUtil.normalizeDigitsOnly(number); return (!allowPrefixMatch || REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT.contains(regionCode)) ? emergencyNumberPattern.matcher(normalizedNumber).matches() : emergencyNumberPattern.matcher(normalizedNumber).lookingAt(); } /** * Given a valid short number, determines whether it is carrier-specific (however, nothing is * implied about its validity). If it is important that the number is valid, then its validity * must first be checked using {@link #isValidShortNumber} or * {@link #isValidShortNumberForRegion}. * * @param number the valid short number to check * @return whether the short number is carrier-specific (assuming the input was a valid short * number). */ public boolean isCarrierSpecific(PhoneNumber number) { List regionCodes = phoneUtil.getRegionCodesForCountryCode(number.getCountryCode()); String regionCode = getRegionCodeForShortNumberFromRegionList(number, regionCodes); String nationalNumber = phoneUtil.getNationalSignificantNumber(number); PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(regionCode); return (phoneMetadata != null) && (phoneUtil.isNumberMatchingDesc(nationalNumber, phoneMetadata.getCarrierSpecific())); } }