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
17package com.android.i18n.phonenumbers;
18
19import com.android.i18n.phonenumbers.Phonemetadata.PhoneMetadata;
20import com.android.i18n.phonenumbers.Phonemetadata.PhoneNumberDesc;
21import com.android.i18n.phonenumbers.Phonenumber.PhoneNumber;
22
23import java.util.Collections;
24import java.util.HashSet;
25import java.util.List;
26import java.util.Set;
27import java.util.logging.Level;
28import java.util.logging.Logger;
29import 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 */
39public 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. */
64  public static ShortNumberInfo getInstance() {
65    return INSTANCE;
66  }
67
68  private final PhoneNumberUtil phoneUtil;
69
70  // @VisibleForTesting
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   */
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   */
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   */
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   */
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   */
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   */
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.
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   */
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
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
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   */
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   */
376  public boolean isEmergencyNumber(String number, String regionCode) {
377    return matchesEmergencyNumberHelper(number, regionCode, false /* doesn't allow prefix match */);
378  }
379
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   */
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