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.google.i18n.phonenumbers; 18 19 import com.google.i18n.phonenumbers.internal.MatcherApi; 20 import com.google.i18n.phonenumbers.internal.RegexBasedMatcher; 21 import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata; 22 import com.google.i18n.phonenumbers.Phonemetadata.PhoneNumberDesc; 23 import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; 24 25 import java.util.ArrayList; 26 import java.util.Arrays; 27 import java.util.Collections; 28 import java.util.HashSet; 29 import java.util.List; 30 import java.util.Map; 31 import java.util.Set; 32 import java.util.logging.Level; 33 import java.util.logging.Logger; 34 import java.util.regex.Pattern; 35 36 /** 37 * Methods for getting information about short phone numbers, such as short codes and emergency 38 * numbers. Note that most commercial short numbers are not handled here, but by the 39 * {@link PhoneNumberUtil}. 40 * 41 * @author Shaopeng Jia 42 * @author David Yonge-Mallo 43 */ 44 public class ShortNumberInfo { 45 private static final Logger logger = Logger.getLogger(ShortNumberInfo.class.getName()); 46 47 private static final ShortNumberInfo INSTANCE = 48 new ShortNumberInfo(RegexBasedMatcher.create()); 49 50 // In these countries, if extra digits are added to an emergency number, it no longer connects 51 // to the emergency service. 52 private static final Set<String> REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT = 53 new HashSet<String>(); 54 static { 55 REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT.add("BR"); 56 REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT.add("CL"); 57 REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT.add("NI"); 58 } 59 60 /** Cost categories of short numbers. */ 61 public enum ShortNumberCost { 62 TOLL_FREE, 63 STANDARD_RATE, 64 PREMIUM_RATE, 65 UNKNOWN_COST 66 } 67 68 /** Returns the singleton instance of the ShortNumberInfo. */ getInstance()69 public static ShortNumberInfo getInstance() { 70 return INSTANCE; 71 } 72 73 // MatcherApi supports the basic matching method for checking if a given national number matches 74 // a national number patten or a possible number patten defined in the given 75 // {@code PhoneNumberDesc}. 76 private final MatcherApi matcherApi; 77 78 // A mapping from a country calling code to the region codes which denote the region represented 79 // by that country calling code. In the case of multiple regions sharing a calling code, such as 80 // the NANPA regions, the one indicated with "isMainCountryForCode" in the metadata should be 81 // first. 82 private final Map<Integer, List<String>> countryCallingCodeToRegionCodeMap; 83 84 // @VisibleForTesting ShortNumberInfo(MatcherApi matcherApi)85 ShortNumberInfo(MatcherApi matcherApi) { 86 this.matcherApi = matcherApi; 87 // TODO: Create ShortNumberInfo for a given map 88 this.countryCallingCodeToRegionCodeMap = 89 CountryCodeToRegionCodeMap.getCountryCodeToRegionCodeMap(); 90 } 91 92 /** 93 * Returns a list with the region codes that match the specific country calling code. For 94 * non-geographical country calling codes, the region code 001 is returned. Also, in the case 95 * of no region code being found, an empty list is returned. 96 */ getRegionCodesForCountryCode(int countryCallingCode)97 private List<String> getRegionCodesForCountryCode(int countryCallingCode) { 98 List<String> regionCodes = countryCallingCodeToRegionCodeMap.get(countryCallingCode); 99 return Collections.unmodifiableList(regionCodes == null ? new ArrayList<String>(0) 100 : regionCodes); 101 } 102 103 /** 104 * Check whether a short number is a possible number when dialled from a region, given the number 105 * in the form of a string, and the region where the number is dialed from. This provides a more 106 * lenient check than {@link #isValidShortNumberForRegion}. 107 * 108 * @param shortNumber the short number to check as a string 109 * @param regionDialingFrom the region from which the number is dialed 110 * @return whether the number is a possible short number 111 * @deprecated Anyone who was using it and passing in a string with whitespace (or other 112 * formatting characters) would have been getting the wrong result. You should parse 113 * the string to PhoneNumber and use the method 114 * {@code #isPossibleShortNumberForRegion(PhoneNumber, String)}. This method will be 115 * removed in the next release. 116 */ 117 @Deprecated isPossibleShortNumberForRegion(String shortNumber, String regionDialingFrom)118 public boolean isPossibleShortNumberForRegion(String shortNumber, String regionDialingFrom) { 119 PhoneMetadata phoneMetadata = 120 MetadataManager.getShortNumberMetadataForRegion(regionDialingFrom); 121 if (phoneMetadata == null) { 122 return false; 123 } 124 return matcherApi.matchesPossibleNumber(shortNumber, phoneMetadata.getGeneralDesc()); 125 } 126 127 /** 128 * Check whether a short number is a possible number when dialed from the given region. This 129 * provides a more lenient check than {@link #isValidShortNumberForRegion}. 130 * 131 * @param number the short number to check 132 * @param regionDialingFrom the region from which the number is dialed 133 * @return whether the number is a possible short number 134 */ isPossibleShortNumberForRegion(PhoneNumber number, String regionDialingFrom)135 public boolean isPossibleShortNumberForRegion(PhoneNumber number, String regionDialingFrom) { 136 PhoneMetadata phoneMetadata = 137 MetadataManager.getShortNumberMetadataForRegion(regionDialingFrom); 138 if (phoneMetadata == null) { 139 return false; 140 } 141 return matcherApi.matchesPossibleNumber(getNationalSignificantNumber(number), 142 phoneMetadata.getGeneralDesc()); 143 } 144 145 /** 146 * Check whether a short number is a possible number. If a country calling code is shared by 147 * multiple regions, this returns true if it's possible in any of them. This provides a more 148 * lenient check than {@link #isValidShortNumber}. See {@link 149 * #isPossibleShortNumberForRegion(PhoneNumber, String)} for details. 150 * 151 * @param number the short number to check 152 * @return whether the number is a possible short number 153 */ isPossibleShortNumber(PhoneNumber number)154 public boolean isPossibleShortNumber(PhoneNumber number) { 155 List<String> regionCodes = getRegionCodesForCountryCode(number.getCountryCode()); 156 String shortNumber = getNationalSignificantNumber(number); 157 for (String region : regionCodes) { 158 PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(region); 159 if (matcherApi.matchesPossibleNumber(shortNumber, phoneMetadata.getGeneralDesc())) { 160 return true; 161 } 162 } 163 return false; 164 } 165 166 /** 167 * Tests whether a short number matches a valid pattern in a region. Note that this doesn't verify 168 * the number is actually in use, which is impossible to tell by just looking at the number 169 * itself. 170 * 171 * @param shortNumber the short number to check as a string 172 * @param regionDialingFrom the region from which the number is dialed 173 * @return whether the short number matches a valid pattern 174 * @deprecated Anyone who was using it and passing in a string with whitespace (or other 175 * formatting characters) would have been getting the wrong result. You should parse 176 * the string to PhoneNumber and use the method 177 * {@code #isValidShortNumberForRegion(PhoneNumber, String)}. This method will be 178 * removed in the next release. 179 */ 180 @Deprecated isValidShortNumberForRegion(String shortNumber, String regionDialingFrom)181 public boolean isValidShortNumberForRegion(String shortNumber, String regionDialingFrom) { 182 PhoneMetadata phoneMetadata = 183 MetadataManager.getShortNumberMetadataForRegion(regionDialingFrom); 184 if (phoneMetadata == null) { 185 return false; 186 } 187 PhoneNumberDesc generalDesc = phoneMetadata.getGeneralDesc(); 188 if (!matchesPossibleNumberAndNationalNumber(shortNumber, generalDesc)) { 189 return false; 190 } 191 PhoneNumberDesc shortNumberDesc = phoneMetadata.getShortCode(); 192 return matchesPossibleNumberAndNationalNumber(shortNumber, shortNumberDesc); 193 } 194 195 /** 196 * Tests whether a short number matches a valid pattern in a region. Note that this doesn't verify 197 * the number is actually in use, which is impossible to tell by just looking at the number 198 * itself. 199 * 200 * @param number the short number for which we want to test the validity 201 * @param regionDialingFrom the region from which the number is dialed 202 * @return whether the short number matches a valid pattern 203 */ isValidShortNumberForRegion(PhoneNumber number, String regionDialingFrom)204 public boolean isValidShortNumberForRegion(PhoneNumber number, String regionDialingFrom) { 205 PhoneMetadata phoneMetadata = 206 MetadataManager.getShortNumberMetadataForRegion(regionDialingFrom); 207 if (phoneMetadata == null) { 208 return false; 209 } 210 String shortNumber = getNationalSignificantNumber(number); 211 PhoneNumberDesc generalDesc = phoneMetadata.getGeneralDesc(); 212 if (!matchesPossibleNumberAndNationalNumber(shortNumber, generalDesc)) { 213 return false; 214 } 215 PhoneNumberDesc shortNumberDesc = phoneMetadata.getShortCode(); 216 return matchesPossibleNumberAndNationalNumber(shortNumber, shortNumberDesc); 217 } 218 219 /** 220 * Tests whether a short number matches a valid pattern. If a country calling code is shared by 221 * multiple regions, this returns true if it's valid in any of them. Note that this doesn't verify 222 * the number is actually in use, which is impossible to tell by just looking at the number 223 * itself. See {@link #isValidShortNumberForRegion(PhoneNumber, String)} for details. 224 * 225 * @param number the short number for which we want to test the validity 226 * @return whether the short number matches a valid pattern 227 */ isValidShortNumber(PhoneNumber number)228 public boolean isValidShortNumber(PhoneNumber number) { 229 List<String> regionCodes = getRegionCodesForCountryCode(number.getCountryCode()); 230 String regionCode = getRegionCodeForShortNumberFromRegionList(number, regionCodes); 231 if (regionCodes.size() > 1 && regionCode != null) { 232 // If a matching region had been found for the phone number from among two or more regions, 233 // then we have already implicitly verified its validity for that region. 234 return true; 235 } 236 return isValidShortNumberForRegion(number, regionCode); 237 } 238 239 /** 240 * Gets the expected cost category of a short number when dialled from a region (however, nothing 241 * is implied about its validity). If it is important that the number is valid, then its validity 242 * must first be checked using {@link isValidShortNumberForRegion}. Note that emergency numbers 243 * are always considered toll-free. Example usage: 244 * <pre>{@code 245 * ShortNumberInfo shortInfo = ShortNumberInfo.getInstance(); 246 * String shortNumber = "110"; 247 * String regionCode = "FR"; 248 * if (shortInfo.isValidShortNumberForRegion(shortNumber, regionCode)) { 249 * ShortNumberInfo.ShortNumberCost cost = shortInfo.getExpectedCostForRegion(shortNumber, 250 * regionCode); 251 * // Do something with the cost information here. 252 * }}</pre> 253 * 254 * @param shortNumber the short number for which we want to know the expected cost category, 255 * as a string 256 * @param regionDialingFrom the region from which the number is dialed 257 * @return the expected cost category for that region of the short number. Returns UNKNOWN_COST if 258 * the number does not match a cost category. Note that an invalid number may match any cost 259 * category. 260 * @deprecated Anyone who was using it and passing in a string with whitespace (or other 261 * formatting characters) would have been getting the wrong result. You should parse 262 * the string to PhoneNumber and use the method 263 * {@code #getExpectedCostForRegion(PhoneNumber, String)}. This method will be 264 * removed in the next release. 265 */ 266 @Deprecated getExpectedCostForRegion(String shortNumber, String regionDialingFrom)267 public ShortNumberCost getExpectedCostForRegion(String shortNumber, String regionDialingFrom) { 268 // Note that regionDialingFrom may be null, in which case phoneMetadata will also be null. 269 PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion( 270 regionDialingFrom); 271 if (phoneMetadata == null) { 272 return ShortNumberCost.UNKNOWN_COST; 273 } 274 275 // The cost categories are tested in order of decreasing expense, since if for some reason the 276 // patterns overlap the most expensive matching cost category should be returned. 277 if (matchesPossibleNumberAndNationalNumber(shortNumber, phoneMetadata.getPremiumRate())) { 278 return ShortNumberCost.PREMIUM_RATE; 279 } 280 if (matchesPossibleNumberAndNationalNumber(shortNumber, phoneMetadata.getStandardRate())) { 281 return ShortNumberCost.STANDARD_RATE; 282 } 283 if (matchesPossibleNumberAndNationalNumber(shortNumber, phoneMetadata.getTollFree())) { 284 return ShortNumberCost.TOLL_FREE; 285 } 286 if (isEmergencyNumber(shortNumber, regionDialingFrom)) { 287 // Emergency numbers are implicitly toll-free. 288 return ShortNumberCost.TOLL_FREE; 289 } 290 return ShortNumberCost.UNKNOWN_COST; 291 } 292 293 /** 294 * Gets the expected cost category of a short number when dialed from a region (however, nothing 295 * is implied about its validity). If it is important that the number is valid, then its validity 296 * must first be checked using {@link #isValidShortNumberForRegion}. Note that emergency numbers 297 * are always considered toll-free. Example usage: 298 * <pre>{@code 299 * // The region for which the number was parsed and the region we subsequently check against 300 * // need not be the same. Here we parse the number in the US and check it for Canada. 301 * PhoneNumber number = phoneUtil.parse("110", "US"); 302 * ... 303 * String regionCode = "CA"; 304 * ShortNumberInfo shortInfo = ShortNumberInfo.getInstance(); 305 * if (shortInfo.isValidShortNumberForRegion(shortNumber, regionCode)) { 306 * ShortNumberCost cost = shortInfo.getExpectedCostForRegion(number, regionCode); 307 * // Do something with the cost information here. 308 * }}</pre> 309 * 310 * @param number the short number for which we want to know the expected cost category 311 * @param regionDialingFrom the region from which the number is dialed 312 * @return the expected cost category for that region of the short number. Returns UNKNOWN_COST if 313 * the number does not match a cost category. Note that an invalid number may match any cost 314 * category. 315 */ getExpectedCostForRegion(PhoneNumber number, String regionDialingFrom)316 public ShortNumberCost getExpectedCostForRegion(PhoneNumber number, String regionDialingFrom) { 317 // Note that regionDialingFrom may be null, in which case phoneMetadata will also be null. 318 PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion( 319 regionDialingFrom); 320 if (phoneMetadata == null) { 321 return ShortNumberCost.UNKNOWN_COST; 322 } 323 324 String shortNumber = getNationalSignificantNumber(number); 325 326 // The cost categories are tested in order of decreasing expense, since if for some reason the 327 // patterns overlap the most expensive matching cost category should be returned. 328 if (matchesPossibleNumberAndNationalNumber(shortNumber, phoneMetadata.getPremiumRate())) { 329 return ShortNumberCost.PREMIUM_RATE; 330 } 331 if (matchesPossibleNumberAndNationalNumber(shortNumber, phoneMetadata.getStandardRate())) { 332 return ShortNumberCost.STANDARD_RATE; 333 } 334 if (matchesPossibleNumberAndNationalNumber(shortNumber, phoneMetadata.getTollFree())) { 335 return ShortNumberCost.TOLL_FREE; 336 } 337 if (isEmergencyNumber(shortNumber, regionDialingFrom)) { 338 // Emergency numbers are implicitly toll-free. 339 return ShortNumberCost.TOLL_FREE; 340 } 341 return ShortNumberCost.UNKNOWN_COST; 342 } 343 344 /** 345 * Gets the expected cost category of a short number (however, nothing is implied about its 346 * validity). If the country calling code is unique to a region, this method behaves exactly the 347 * same as {@link #getExpectedCostForRegion(PhoneNumber, String)}. However, if the country 348 * calling code is shared by multiple regions, then it returns the highest cost in the sequence 349 * PREMIUM_RATE, UNKNOWN_COST, STANDARD_RATE, TOLL_FREE. The reason for the position of 350 * UNKNOWN_COST in this order is that if a number is UNKNOWN_COST in one region but STANDARD_RATE 351 * or TOLL_FREE in another, its expected cost cannot be estimated as one of the latter since it 352 * might be a PREMIUM_RATE number. 353 * <p> 354 * For example, if a number is STANDARD_RATE in the US, but TOLL_FREE in Canada, the expected 355 * cost returned by this method will be STANDARD_RATE, since the NANPA countries share the same 356 * country calling code. 357 * <p> 358 * Note: If the region from which the number is dialed is known, it is highly preferable to call 359 * {@link #getExpectedCostForRegion(PhoneNumber, String)} instead. 360 * 361 * @param number the short number for which we want to know the expected cost category 362 * @return the highest expected cost category of the short number in the region(s) with the given 363 * country calling code 364 */ getExpectedCost(PhoneNumber number)365 public ShortNumberCost getExpectedCost(PhoneNumber number) { 366 List<String> regionCodes = getRegionCodesForCountryCode(number.getCountryCode()); 367 if (regionCodes.size() == 0) { 368 return ShortNumberCost.UNKNOWN_COST; 369 } 370 if (regionCodes.size() == 1) { 371 return getExpectedCostForRegion(number, regionCodes.get(0)); 372 } 373 ShortNumberCost cost = ShortNumberCost.TOLL_FREE; 374 for (String regionCode : regionCodes) { 375 ShortNumberCost costForRegion = getExpectedCostForRegion(number, regionCode); 376 switch (costForRegion) { 377 case PREMIUM_RATE: 378 return ShortNumberCost.PREMIUM_RATE; 379 case UNKNOWN_COST: 380 cost = ShortNumberCost.UNKNOWN_COST; 381 break; 382 case STANDARD_RATE: 383 if (cost != ShortNumberCost.UNKNOWN_COST) { 384 cost = ShortNumberCost.STANDARD_RATE; 385 } 386 break; 387 case TOLL_FREE: 388 // Do nothing. 389 break; 390 default: 391 logger.log(Level.SEVERE, "Unrecognised cost for region: " + costForRegion); 392 } 393 } 394 return cost; 395 } 396 397 // Helper method to get the region code for a given phone number, from a list of possible region 398 // codes. If the list contains more than one region, the first region for which the number is 399 // valid is returned. getRegionCodeForShortNumberFromRegionList(PhoneNumber number, List<String> regionCodes)400 private String getRegionCodeForShortNumberFromRegionList(PhoneNumber number, 401 List<String> regionCodes) { 402 if (regionCodes.size() == 0) { 403 return null; 404 } else if (regionCodes.size() == 1) { 405 return regionCodes.get(0); 406 } 407 String nationalNumber = getNationalSignificantNumber(number); 408 for (String regionCode : regionCodes) { 409 PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(regionCode); 410 if (phoneMetadata != null 411 && matchesPossibleNumberAndNationalNumber(nationalNumber, phoneMetadata.getShortCode())) { 412 // The number is valid for this region. 413 return regionCode; 414 } 415 } 416 return null; 417 } 418 419 /** 420 * Convenience method to get a list of what regions the library has metadata for. 421 */ getSupportedRegions()422 Set<String> getSupportedRegions() { 423 return Collections.unmodifiableSet(MetadataManager.getShortNumberMetadataSupportedRegions()); 424 } 425 426 /** 427 * Gets a valid short number for the specified region. 428 * 429 * @param regionCode the region for which an example short number is needed 430 * @return a valid short number for the specified region. Returns an empty string when the 431 * metadata does not contain such information. 432 */ 433 // @VisibleForTesting getExampleShortNumber(String regionCode)434 String getExampleShortNumber(String regionCode) { 435 PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(regionCode); 436 if (phoneMetadata == null) { 437 return ""; 438 } 439 PhoneNumberDesc desc = phoneMetadata.getShortCode(); 440 if (desc.hasExampleNumber()) { 441 return desc.getExampleNumber(); 442 } 443 return ""; 444 } 445 446 /** 447 * Gets a valid short number for the specified cost category. 448 * 449 * @param regionCode the region for which an example short number is needed 450 * @param cost the cost category of number that is needed 451 * @return a valid short number for the specified region and cost category. Returns an empty 452 * string when the metadata does not contain such information, or the cost is UNKNOWN_COST. 453 */ 454 // @VisibleForTesting getExampleShortNumberForCost(String regionCode, ShortNumberCost cost)455 String getExampleShortNumberForCost(String regionCode, ShortNumberCost cost) { 456 PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(regionCode); 457 if (phoneMetadata == null) { 458 return ""; 459 } 460 PhoneNumberDesc desc = null; 461 switch (cost) { 462 case TOLL_FREE: 463 desc = phoneMetadata.getTollFree(); 464 break; 465 case STANDARD_RATE: 466 desc = phoneMetadata.getStandardRate(); 467 break; 468 case PREMIUM_RATE: 469 desc = phoneMetadata.getPremiumRate(); 470 break; 471 default: 472 // UNKNOWN_COST numbers are computed by the process of elimination from the other cost 473 // categories. 474 } 475 if (desc != null && desc.hasExampleNumber()) { 476 return desc.getExampleNumber(); 477 } 478 return ""; 479 } 480 481 /** 482 * Returns true if the given number, exactly as dialed, might be used to connect to an emergency 483 * service in the given region. 484 * <p> 485 * This method accepts a string, rather than a PhoneNumber, because it needs to distinguish 486 * cases such as "+1 911" and "911", where the former may not connect to an emergency service in 487 * all cases but the latter would. This method takes into account cases where the number might 488 * contain formatting, or might have additional digits appended (when it is okay to do that in 489 * the specified region). 490 * 491 * @param number the phone number to test 492 * @param regionCode the region where the phone number is being dialed 493 * @return whether the number might be used to connect to an emergency service in the given region 494 */ connectsToEmergencyNumber(String number, String regionCode)495 public boolean connectsToEmergencyNumber(String number, String regionCode) { 496 return matchesEmergencyNumberHelper(number, regionCode, true /* allows prefix match */); 497 } 498 499 /** 500 * Returns true if the given number exactly matches an emergency service number in the given 501 * region. 502 * <p> 503 * This method takes into account cases where the number might contain formatting, but doesn't 504 * allow additional digits to be appended. Note that {@code isEmergencyNumber(number, region)} 505 * implies {@code connectsToEmergencyNumber(number, region)}. 506 * 507 * @param number the phone number to test 508 * @param regionCode the region where the phone number is being dialed 509 * @return whether the number exactly matches an emergency services number in the given region 510 */ isEmergencyNumber(String number, String regionCode)511 public boolean isEmergencyNumber(String number, String regionCode) { 512 return matchesEmergencyNumberHelper(number, regionCode, false /* doesn't allow prefix match */); 513 } 514 matchesEmergencyNumberHelper(String number, String regionCode, boolean allowPrefixMatch)515 private boolean matchesEmergencyNumberHelper(String number, String regionCode, 516 boolean allowPrefixMatch) { 517 number = PhoneNumberUtil.extractPossibleNumber(number); 518 if (PhoneNumberUtil.PLUS_CHARS_PATTERN.matcher(number).lookingAt()) { 519 // Returns false if the number starts with a plus sign. We don't believe dialing the country 520 // code before emergency numbers (e.g. +1911) works, but later, if that proves to work, we can 521 // add additional logic here to handle it. 522 return false; 523 } 524 PhoneMetadata metadata = MetadataManager.getShortNumberMetadataForRegion(regionCode); 525 if (metadata == null || !metadata.hasEmergency()) { 526 return false; 527 } 528 529 String normalizedNumber = PhoneNumberUtil.normalizeDigitsOnly(number); 530 PhoneNumberDesc emergencyDesc = metadata.getEmergency(); 531 boolean allowPrefixMatchForRegion = 532 allowPrefixMatch && !REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT.contains(regionCode); 533 return matcherApi.matchesNationalNumber(normalizedNumber, emergencyDesc, 534 allowPrefixMatchForRegion); 535 } 536 537 /** 538 * Given a valid short number, determines whether it is carrier-specific (however, nothing is 539 * implied about its validity). If it is important that the number is valid, then its validity 540 * must first be checked using {@link #isValidShortNumber} or 541 * {@link #isValidShortNumberForRegion}. 542 * 543 * @param number the valid short number to check 544 * @return whether the short number is carrier-specific (assuming the input was a valid short 545 * number). 546 */ isCarrierSpecific(PhoneNumber number)547 public boolean isCarrierSpecific(PhoneNumber number) { 548 List<String> regionCodes = getRegionCodesForCountryCode(number.getCountryCode()); 549 String regionCode = getRegionCodeForShortNumberFromRegionList(number, regionCodes); 550 String nationalNumber = getNationalSignificantNumber(number); 551 PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(regionCode); 552 return (phoneMetadata != null) 553 && (matchesPossibleNumberAndNationalNumber(nationalNumber, 554 phoneMetadata.getCarrierSpecific())); 555 } 556 557 /** 558 * Gets the national significant number of the a phone number. Note a national significant number 559 * doesn't contain a national prefix or any formatting. 560 * <p> 561 * This is a temporary duplicate of the {@code getNationalSignificantNumber} method from 562 * {@code PhoneNumberUtil}. Ultimately a canonical static version should exist in a separate 563 * utility class (to prevent {@code ShortNumberInfo} needing to depend on PhoneNumberUtil). 564 * 565 * @param number the phone number for which the national significant number is needed 566 * @return the national significant number of the PhoneNumber object passed in 567 */ getNationalSignificantNumber(PhoneNumber number)568 private static String getNationalSignificantNumber(PhoneNumber number) { 569 // If leading zero(s) have been set, we prefix this now. Note this is not a national prefix. 570 StringBuilder nationalNumber = new StringBuilder(); 571 if (number.isItalianLeadingZero()) { 572 char[] zeros = new char[number.getNumberOfLeadingZeros()]; 573 Arrays.fill(zeros, '0'); 574 nationalNumber.append(new String(zeros)); 575 } 576 nationalNumber.append(number.getNationalNumber()); 577 return nationalNumber.toString(); 578 } 579 580 // TODO: Once we have benchmarked ShortNumberInfo, consider if it is worth keeping 581 // this performance optimization, and if so move this into the matcher implementation. matchesPossibleNumberAndNationalNumber(String number, PhoneNumberDesc numberDesc)582 private boolean matchesPossibleNumberAndNationalNumber(String number, 583 PhoneNumberDesc numberDesc) { 584 return matcherApi.matchesPossibleNumber(number, numberDesc) 585 && matcherApi.matchesNationalNumber(number, numberDesc, false); 586 } 587 } 588