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