1 /* 2 * Copyright (C) 2013 The Libphonenumber Authors 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.i18n.phonenumbers; 18 19 import com.android.i18n.phonenumbers.Phonemetadata.PhoneMetadata; 20 import com.android.i18n.phonenumbers.Phonemetadata.PhoneNumberDesc; 21 import com.android.i18n.phonenumbers.Phonenumber.PhoneNumber; 22 23 import java.util.Collections; 24 import java.util.HashSet; 25 import java.util.List; 26 import java.util.Set; 27 import java.util.logging.Level; 28 import java.util.logging.Logger; 29 import java.util.regex.Pattern; 30 31 /** 32 * Methods for getting information about short phone numbers, such as short codes and emergency 33 * numbers. Note that most commercial short numbers are not handled here, but by the 34 * {@link PhoneNumberUtil}. 35 * 36 * @author Shaopeng Jia 37 * @author David Yonge-Mallo 38 */ 39 public class ShortNumberInfo { 40 private static final Logger logger = Logger.getLogger(ShortNumberInfo.class.getName()); 41 42 private static final ShortNumberInfo INSTANCE = 43 new ShortNumberInfo(PhoneNumberUtil.getInstance()); 44 45 // In these countries, if extra digits are added to an emergency number, it no longer connects 46 // to the emergency service. 47 private static final Set<String> REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT = 48 new HashSet<String>(); 49 static { 50 REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT.add("BR"); 51 REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT.add("CL"); 52 REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT.add("NI"); 53 } 54 55 /** Cost categories of short numbers. */ 56 public enum ShortNumberCost { 57 TOLL_FREE, 58 STANDARD_RATE, 59 PREMIUM_RATE, 60 UNKNOWN_COST 61 } 62 63 /** Returns the singleton instance of the ShortNumberInfo. */ getInstance()64 public static ShortNumberInfo getInstance() { 65 return INSTANCE; 66 } 67 68 private final PhoneNumberUtil phoneUtil; 69 70 // @VisibleForTesting ShortNumberInfo(PhoneNumberUtil util)71 ShortNumberInfo(PhoneNumberUtil util) { 72 phoneUtil = util; 73 } 74 75 /** 76 * Check whether a short number is a possible number when dialled from a region, given the number 77 * in the form of a string, and the region where the number is dialed from. This provides a more 78 * lenient check than {@link #isValidShortNumberForRegion}. 79 * 80 * @param shortNumber the short number to check as a string 81 * @param regionDialingFrom the region from which the number is dialed 82 * @return whether the number is a possible short number 83 */ isPossibleShortNumberForRegion(String shortNumber, String regionDialingFrom)84 public boolean isPossibleShortNumberForRegion(String shortNumber, String regionDialingFrom) { 85 PhoneMetadata phoneMetadata = 86 MetadataManager.getShortNumberMetadataForRegion(regionDialingFrom); 87 if (phoneMetadata == null) { 88 return false; 89 } 90 PhoneNumberDesc generalDesc = phoneMetadata.getGeneralDesc(); 91 return phoneUtil.isNumberPossibleForDesc(shortNumber, generalDesc); 92 } 93 94 /** 95 * Check whether a short number is a possible number. If a country calling code is shared by 96 * multiple regions, this returns true if it's possible in any of them. This provides a more 97 * lenient check than {@link #isValidShortNumber}. See {@link 98 * #isPossibleShortNumberForRegion(String, String)} for details. 99 * 100 * @param number the short number to check 101 * @return whether the number is a possible short number 102 */ isPossibleShortNumber(PhoneNumber number)103 public boolean isPossibleShortNumber(PhoneNumber number) { 104 List<String> regionCodes = phoneUtil.getRegionCodesForCountryCode(number.getCountryCode()); 105 String shortNumber = phoneUtil.getNationalSignificantNumber(number); 106 for (String region : regionCodes) { 107 PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(region); 108 if (phoneUtil.isNumberPossibleForDesc(shortNumber, phoneMetadata.getGeneralDesc())) { 109 return true; 110 } 111 } 112 return false; 113 } 114 115 /** 116 * Tests whether a short number matches a valid pattern in a region. Note that this doesn't verify 117 * the number is actually in use, which is impossible to tell by just looking at the number 118 * itself. 119 * 120 * @param shortNumber the short number to check as a string 121 * @param regionDialingFrom the region from which the number is dialed 122 * @return whether the short number matches a valid pattern 123 */ isValidShortNumberForRegion(String shortNumber, String regionDialingFrom)124 public boolean isValidShortNumberForRegion(String shortNumber, String regionDialingFrom) { 125 PhoneMetadata phoneMetadata = 126 MetadataManager.getShortNumberMetadataForRegion(regionDialingFrom); 127 if (phoneMetadata == null) { 128 return false; 129 } 130 PhoneNumberDesc generalDesc = phoneMetadata.getGeneralDesc(); 131 if (!generalDesc.hasNationalNumberPattern() || 132 !phoneUtil.isNumberMatchingDesc(shortNumber, generalDesc)) { 133 return false; 134 } 135 PhoneNumberDesc shortNumberDesc = phoneMetadata.getShortCode(); 136 if (!shortNumberDesc.hasNationalNumberPattern()) { 137 logger.log(Level.WARNING, "No short code national number pattern found for region: " + 138 regionDialingFrom); 139 return false; 140 } 141 return phoneUtil.isNumberMatchingDesc(shortNumber, shortNumberDesc); 142 } 143 144 /** 145 * Tests whether a short number matches a valid pattern. If a country calling code is shared by 146 * multiple regions, this returns true if it's valid in any of them. Note that this doesn't verify 147 * the number is actually in use, which is impossible to tell by just looking at the number 148 * itself. See {@link #isValidShortNumberForRegion(String, String)} for details. 149 * 150 * @param number the short number for which we want to test the validity 151 * @return whether the short number matches a valid pattern 152 */ isValidShortNumber(PhoneNumber number)153 public boolean isValidShortNumber(PhoneNumber number) { 154 List<String> regionCodes = phoneUtil.getRegionCodesForCountryCode(number.getCountryCode()); 155 String shortNumber = phoneUtil.getNationalSignificantNumber(number); 156 String regionCode = getRegionCodeForShortNumberFromRegionList(number, regionCodes); 157 if (regionCodes.size() > 1 && regionCode != null) { 158 // If a matching region had been found for the phone number from among two or more regions, 159 // then we have already implicitly verified its validity for that region. 160 return true; 161 } 162 return isValidShortNumberForRegion(shortNumber, regionCode); 163 } 164 165 /** 166 * Gets the expected cost category of a short number when dialled from a region (however, nothing 167 * is implied about its validity). If it is important that the number is valid, then its validity 168 * must first be checked using {@link isValidShortNumberForRegion}. Note that emergency numbers 169 * are always considered toll-free. Example usage: 170 * <pre>{@code 171 * ShortNumberInfo shortInfo = ShortNumberInfo.getInstance(); 172 * String shortNumber = "110"; 173 * String regionCode = "FR"; 174 * if (shortInfo.isValidShortNumberForRegion(shortNumber, regionCode)) { 175 * ShortNumberInfo.ShortNumberCost cost = shortInfo.getExpectedCostForRegion(shortNumber, 176 * regionCode); 177 * // Do something with the cost information here. 178 * }}</pre> 179 * 180 * @param shortNumber the short number for which we want to know the expected cost category, 181 * as a string 182 * @param regionDialingFrom the region from which the number is dialed 183 * @return the expected cost category for that region of the short number. Returns UNKNOWN_COST if 184 * the number does not match a cost category. Note that an invalid number may match any cost 185 * category. 186 */ getExpectedCostForRegion(String shortNumber, String regionDialingFrom)187 public ShortNumberCost getExpectedCostForRegion(String shortNumber, String regionDialingFrom) { 188 // Note that regionDialingFrom may be null, in which case phoneMetadata will also be null. 189 PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion( 190 regionDialingFrom); 191 if (phoneMetadata == null) { 192 return ShortNumberCost.UNKNOWN_COST; 193 } 194 195 // The cost categories are tested in order of decreasing expense, since if for some reason the 196 // patterns overlap the most expensive matching cost category should be returned. 197 if (phoneUtil.isNumberMatchingDesc(shortNumber, phoneMetadata.getPremiumRate())) { 198 return ShortNumberCost.PREMIUM_RATE; 199 } 200 if (phoneUtil.isNumberMatchingDesc(shortNumber, phoneMetadata.getStandardRate())) { 201 return ShortNumberCost.STANDARD_RATE; 202 } 203 if (phoneUtil.isNumberMatchingDesc(shortNumber, phoneMetadata.getTollFree())) { 204 return ShortNumberCost.TOLL_FREE; 205 } 206 if (isEmergencyNumber(shortNumber, regionDialingFrom)) { 207 // Emergency numbers are implicitly toll-free. 208 return ShortNumberCost.TOLL_FREE; 209 } 210 return ShortNumberCost.UNKNOWN_COST; 211 } 212 213 /** 214 * Gets the expected cost category of a short number (however, nothing is implied about its 215 * validity). If the country calling code is unique to a region, this method behaves exactly the 216 * same as {@link #getExpectedCostForRegion(String, String)}. However, if the country calling 217 * code is shared by multiple regions, then it returns the highest cost in the sequence 218 * PREMIUM_RATE, UNKNOWN_COST, STANDARD_RATE, TOLL_FREE. The reason for the position of 219 * UNKNOWN_COST in this order is that if a number is UNKNOWN_COST in one region but STANDARD_RATE 220 * or TOLL_FREE in another, its expected cost cannot be estimated as one of the latter since it 221 * might be a PREMIUM_RATE number. 222 * 223 * For example, if a number is STANDARD_RATE in the US, but TOLL_FREE in Canada, the expected cost 224 * returned by this method will be STANDARD_RATE, since the NANPA countries share the same country 225 * calling code. 226 * 227 * Note: If the region from which the number is dialed is known, it is highly preferable to call 228 * {@link #getExpectedCostForRegion(String, String)} instead. 229 * 230 * @param number the short number for which we want to know the expected cost category 231 * @return the highest expected cost category of the short number in the region(s) with the given 232 * country calling code 233 */ getExpectedCost(PhoneNumber number)234 public ShortNumberCost getExpectedCost(PhoneNumber number) { 235 List<String> regionCodes = phoneUtil.getRegionCodesForCountryCode(number.getCountryCode()); 236 if (regionCodes.size() == 0) { 237 return ShortNumberCost.UNKNOWN_COST; 238 } 239 String shortNumber = phoneUtil.getNationalSignificantNumber(number); 240 if (regionCodes.size() == 1) { 241 return getExpectedCostForRegion(shortNumber, regionCodes.get(0)); 242 } 243 ShortNumberCost cost = ShortNumberCost.TOLL_FREE; 244 for (String regionCode : regionCodes) { 245 ShortNumberCost costForRegion = getExpectedCostForRegion(shortNumber, regionCode); 246 switch (costForRegion) { 247 case PREMIUM_RATE: 248 return ShortNumberCost.PREMIUM_RATE; 249 case UNKNOWN_COST: 250 cost = ShortNumberCost.UNKNOWN_COST; 251 break; 252 case STANDARD_RATE: 253 if (cost != ShortNumberCost.UNKNOWN_COST) { 254 cost = ShortNumberCost.STANDARD_RATE; 255 } 256 break; 257 case TOLL_FREE: 258 // Do nothing. 259 break; 260 default: 261 logger.log(Level.SEVERE, "Unrecognised cost for region: " + costForRegion); 262 } 263 } 264 return cost; 265 } 266 267 // Helper method to get the region code for a given phone number, from a list of possible region 268 // codes. If the list contains more than one region, the first region for which the number is 269 // valid is returned. getRegionCodeForShortNumberFromRegionList(PhoneNumber number, List<String> regionCodes)270 private String getRegionCodeForShortNumberFromRegionList(PhoneNumber number, 271 List<String> regionCodes) { 272 if (regionCodes.size() == 0) { 273 return null; 274 } else if (regionCodes.size() == 1) { 275 return regionCodes.get(0); 276 } 277 String nationalNumber = phoneUtil.getNationalSignificantNumber(number); 278 for (String regionCode : regionCodes) { 279 PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(regionCode); 280 if (phoneMetadata != null && 281 phoneUtil.isNumberMatchingDesc(nationalNumber, phoneMetadata.getShortCode())) { 282 // The number is valid for this region. 283 return regionCode; 284 } 285 } 286 return null; 287 } 288 289 /** 290 * Convenience method to get a list of what regions the library has metadata for. 291 */ getSupportedRegions()292 Set<String> getSupportedRegions() { 293 return Collections.unmodifiableSet(MetadataManager.getShortNumberMetadataSupportedRegions()); 294 } 295 296 /** 297 * Gets a valid short number for the specified region. 298 * 299 * @param regionCode the region for which an example short number is needed 300 * @return a valid short number for the specified region. Returns an empty string when the 301 * metadata does not contain such information. 302 */ 303 // @VisibleForTesting getExampleShortNumber(String regionCode)304 String getExampleShortNumber(String regionCode) { 305 PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(regionCode); 306 if (phoneMetadata == null) { 307 return ""; 308 } 309 PhoneNumberDesc desc = phoneMetadata.getShortCode(); 310 if (desc.hasExampleNumber()) { 311 return desc.getExampleNumber(); 312 } 313 return ""; 314 } 315 316 /** 317 * Gets a valid short number for the specified cost category. 318 * 319 * @param regionCode the region for which an example short number is needed 320 * @param cost the cost category of number that is needed 321 * @return a valid short number for the specified region and cost category. Returns an empty 322 * string when the metadata does not contain such information, or the cost is UNKNOWN_COST. 323 */ 324 // @VisibleForTesting getExampleShortNumberForCost(String regionCode, ShortNumberCost cost)325 String getExampleShortNumberForCost(String regionCode, ShortNumberCost cost) { 326 PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(regionCode); 327 if (phoneMetadata == null) { 328 return ""; 329 } 330 PhoneNumberDesc desc = null; 331 switch (cost) { 332 case TOLL_FREE: 333 desc = phoneMetadata.getTollFree(); 334 break; 335 case STANDARD_RATE: 336 desc = phoneMetadata.getStandardRate(); 337 break; 338 case PREMIUM_RATE: 339 desc = phoneMetadata.getPremiumRate(); 340 break; 341 default: 342 // UNKNOWN_COST numbers are computed by the process of elimination from the other cost 343 // categories. 344 } 345 if (desc != null && desc.hasExampleNumber()) { 346 return desc.getExampleNumber(); 347 } 348 return ""; 349 } 350 351 /** 352 * Returns true if the number might be used to connect to an emergency service in the given 353 * region. 354 * 355 * This method takes into account cases where the number might contain formatting, or might have 356 * additional digits appended (when it is okay to do that in the region specified). 357 * 358 * @param number the phone number to test 359 * @param regionCode the region where the phone number is being dialed 360 * @return whether the number might be used to connect to an emergency service in the given region 361 */ connectsToEmergencyNumber(String number, String regionCode)362 public boolean connectsToEmergencyNumber(String number, String regionCode) { 363 return matchesEmergencyNumberHelper(number, regionCode, true /* allows prefix match */); 364 } 365 366 /** 367 * Returns true if the number exactly matches an emergency service number in the given region. 368 * 369 * This method takes into account cases where the number might contain formatting, but doesn't 370 * allow additional digits to be appended. 371 * 372 * @param number the phone number to test 373 * @param regionCode the region where the phone number is being dialed 374 * @return whether the number exactly matches an emergency services number in the given region 375 */ isEmergencyNumber(String number, String regionCode)376 public boolean isEmergencyNumber(String number, String regionCode) { 377 return matchesEmergencyNumberHelper(number, regionCode, false /* doesn't allow prefix match */); 378 } 379 matchesEmergencyNumberHelper(String number, String regionCode, boolean allowPrefixMatch)380 private boolean matchesEmergencyNumberHelper(String number, String regionCode, 381 boolean allowPrefixMatch) { 382 number = PhoneNumberUtil.extractPossibleNumber(number); 383 if (PhoneNumberUtil.PLUS_CHARS_PATTERN.matcher(number).lookingAt()) { 384 // Returns false if the number starts with a plus sign. We don't believe dialing the country 385 // code before emergency numbers (e.g. +1911) works, but later, if that proves to work, we can 386 // add additional logic here to handle it. 387 return false; 388 } 389 PhoneMetadata metadata = MetadataManager.getShortNumberMetadataForRegion(regionCode); 390 if (metadata == null || !metadata.hasEmergency()) { 391 return false; 392 } 393 Pattern emergencyNumberPattern = 394 Pattern.compile(metadata.getEmergency().getNationalNumberPattern()); 395 String normalizedNumber = PhoneNumberUtil.normalizeDigitsOnly(number); 396 return (!allowPrefixMatch || REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT.contains(regionCode)) 397 ? emergencyNumberPattern.matcher(normalizedNumber).matches() 398 : emergencyNumberPattern.matcher(normalizedNumber).lookingAt(); 399 } 400 401 /** 402 * Given a valid short number, determines whether it is carrier-specific (however, nothing is 403 * implied about its validity). If it is important that the number is valid, then its validity 404 * must first be checked using {@link #isValidShortNumber} or 405 * {@link #isValidShortNumberForRegion}. 406 * 407 * @param number the valid short number to check 408 * @return whether the short number is carrier-specific (assuming the input was a valid short 409 * number). 410 */ isCarrierSpecific(PhoneNumber number)411 public boolean isCarrierSpecific(PhoneNumber number) { 412 List<String> regionCodes = phoneUtil.getRegionCodesForCountryCode(number.getCountryCode()); 413 String regionCode = getRegionCodeForShortNumberFromRegionList(number, regionCodes); 414 String nationalNumber = phoneUtil.getNationalSignificantNumber(number); 415 PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(regionCode); 416 return (phoneMetadata != null) && 417 (phoneUtil.isNumberMatchingDesc(nationalNumber, phoneMetadata.getCarrierSpecific())); 418 } 419 } 420