1/*
2 * Copyright (C) 2011 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.geocoding;
18
19import com.android.i18n.phonenumbers.NumberParseException;
20import com.android.i18n.phonenumbers.PhoneNumberUtil;
21import com.android.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberType;
22import com.android.i18n.phonenumbers.Phonenumber.PhoneNumber;
23import com.android.i18n.phonenumbers.prefixmapper.PrefixFileReader;
24
25import java.util.Locale;
26
27/**
28 * An offline geocoder which provides geographical information related to a phone number.
29 *
30 * @author Shaopeng Jia
31 */
32public class PhoneNumberOfflineGeocoder {
33  private static PhoneNumberOfflineGeocoder instance = null;
34  private static final String MAPPING_DATA_DIRECTORY =
35      "/com/android/i18n/phonenumbers/geocoding/data/";
36  private PrefixFileReader prefixFileReader = null;
37
38  private final PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
39
40  // @VisibleForTesting
41  PhoneNumberOfflineGeocoder(String phonePrefixDataDirectory) {
42    prefixFileReader = new PrefixFileReader(phonePrefixDataDirectory);
43  }
44
45  /**
46   * Gets a {@link PhoneNumberOfflineGeocoder} instance to carry out international phone number
47   * geocoding.
48   *
49   * <p> The {@link PhoneNumberOfflineGeocoder} is implemented as a singleton. Therefore, calling
50   * this method multiple times will only result in one instance being created.
51   *
52   * @return  a {@link PhoneNumberOfflineGeocoder} instance
53   */
54  public static synchronized PhoneNumberOfflineGeocoder getInstance() {
55    if (instance == null) {
56      instance = new PhoneNumberOfflineGeocoder(MAPPING_DATA_DIRECTORY);
57    }
58    return instance;
59  }
60
61  /**
62   * Returns the customary display name in the given language for the given territory the phone
63   * number is from.
64   */
65  private String getCountryNameForNumber(PhoneNumber number, Locale language) {
66    String regionCode = phoneUtil.getRegionCodeForNumber(number);
67    return getRegionDisplayName(regionCode, language);
68  }
69
70  /**
71   * Returns the customary display name in the given language for the given region.
72   */
73  private String getRegionDisplayName(String regionCode, Locale language) {
74    return (regionCode == null || regionCode.equals("ZZ") ||
75            regionCode.equals(PhoneNumberUtil.REGION_CODE_FOR_NON_GEO_ENTITY))
76        ? "" : new Locale("", regionCode).getDisplayCountry(language);
77  }
78
79  /**
80   * Returns a text description for the given phone number, in the language provided. The
81   * description might consist of the name of the country where the phone number is from, or the
82   * name of the geographical area the phone number is from if more detailed information is
83   * available.
84   *
85   * <p>This method assumes the validity of the number passed in has already been checked, and that
86   * the number is suitable for geocoding. We consider fixed-line and mobile numbers possible
87   * candidates for geocoding.
88   *
89   * @param number  a valid phone number for which we want to get a text description
90   * @param languageCode  the language code for which the description should be written
91   * @return  a text description for the given language code for the given phone number
92   */
93  public String getDescriptionForValidNumber(PhoneNumber number, Locale languageCode) {
94    String langStr = languageCode.getLanguage();
95    String scriptStr = "";  // No script is specified
96    String regionStr = languageCode.getCountry();
97
98    String areaDescription;
99    String mobileToken = PhoneNumberUtil.getCountryMobileToken(number.getCountryCode());
100    String nationalNumber = phoneUtil.getNationalSignificantNumber(number);
101    if (!mobileToken.equals("") && nationalNumber.startsWith(mobileToken)) {
102      // In some countries, eg. Argentina, mobile numbers have a mobile token before the national
103      // destination code, this should be removed before geocoding.
104      nationalNumber = nationalNumber.substring(mobileToken.length());
105      String region = phoneUtil.getRegionCodeForCountryCode(number.getCountryCode());
106      PhoneNumber copiedNumber;
107      try {
108        copiedNumber = phoneUtil.parse(nationalNumber, region);
109      } catch (NumberParseException e) {
110        // If this happens, just reuse what we had.
111        copiedNumber = number;
112      }
113      areaDescription = prefixFileReader.getDescriptionForNumber(copiedNumber, langStr, scriptStr,
114                                                                 regionStr);
115    } else {
116      areaDescription = prefixFileReader.getDescriptionForNumber(number, langStr, scriptStr,
117                                                                 regionStr);
118    }
119    return (areaDescription.length() > 0)
120        ? areaDescription : getCountryNameForNumber(number, languageCode);
121  }
122
123  /**
124   * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale)} but also considers the
125   * region of the user. If the phone number is from the same region as the user, only a lower-level
126   * description will be returned, if one exists. Otherwise, the phone number's region will be
127   * returned, with optionally some more detailed information.
128   *
129   * <p>For example, for a user from the region "US" (United States), we would show "Mountain View,
130   * CA" for a particular number, omitting the United States from the description. For a user from
131   * the United Kingdom (region "GB"), for the same number we may show "Mountain View, CA, United
132   * States" or even just "United States".
133   *
134   * <p>This method assumes the validity of the number passed in has already been checked.
135   *
136   * @param number  the phone number for which we want to get a text description
137   * @param languageCode  the language code for which the description should be written
138   * @param userRegion  the region code for a given user. This region will be omitted from the
139   *     description if the phone number comes from this region. It is a two-letter uppercase ISO
140   *     country code as defined by ISO 3166-1.
141   * @return  a text description for the given language code for the given phone number, or empty
142   *     string if the number passed in is invalid
143   */
144  public String getDescriptionForValidNumber(PhoneNumber number, Locale languageCode,
145                                             String userRegion) {
146    // If the user region matches the number's region, then we just show the lower-level
147    // description, if one exists - if no description exists, we will show the region(country) name
148    // for the number.
149    String regionCode = phoneUtil.getRegionCodeForNumber(number);
150    if (userRegion.equals(regionCode)) {
151      return getDescriptionForValidNumber(number, languageCode);
152    }
153    // Otherwise, we just show the region(country) name for now.
154    return getRegionDisplayName(regionCode, languageCode);
155    // TODO: Concatenate the lower-level and country-name information in an appropriate
156    // way for each language.
157  }
158
159  /**
160   * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale)} but explicitly checks
161   * the validity of the number passed in.
162   *
163   * @param number  the phone number for which we want to get a text description
164   * @param languageCode  the language code for which the description should be written
165   * @return  a text description for the given language code for the given phone number, or empty
166   *     string if the number passed in is invalid
167   */
168  public String getDescriptionForNumber(PhoneNumber number, Locale languageCode) {
169    PhoneNumberType numberType = phoneUtil.getNumberType(number);
170    if (numberType == PhoneNumberType.UNKNOWN) {
171      return "";
172    } else if (!canBeGeocoded(numberType)) {
173      return getCountryNameForNumber(number, languageCode);
174    }
175    return getDescriptionForValidNumber(number, languageCode);
176  }
177
178  /**
179   * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale, String)} but
180   * explicitly checks the validity of the number passed in.
181   *
182   * @param number  the phone number for which we want to get a text description
183   * @param languageCode  the language code for which the description should be written
184   * @param userRegion  the region code for a given user. This region will be omitted from the
185   *     description if the phone number comes from this region. It is a two-letter uppercase ISO
186   *     country code as defined by ISO 3166-1.
187   * @return  a text description for the given language code for the given phone number, or empty
188   *     string if the number passed in is invalid
189   */
190  public String getDescriptionForNumber(PhoneNumber number, Locale languageCode,
191                                        String userRegion) {
192    PhoneNumberType numberType = phoneUtil.getNumberType(number);
193    if (numberType == PhoneNumberType.UNKNOWN) {
194      return "";
195    } else if (!canBeGeocoded(numberType)) {
196      return getCountryNameForNumber(number, languageCode);
197    }
198    return getDescriptionForValidNumber(number, languageCode, userRegion);
199  }
200
201  /**
202   * A similar method is implemented as PhoneNumberUtil.isNumberGeographical, which performs a
203   * stricter check, as it determines if a number has a geographical association. Also, if new
204   * phone number types were added, we should check if this other method should be updated too.
205   */
206  private boolean canBeGeocoded(PhoneNumberType numberType) {
207    return (numberType == PhoneNumberType.FIXED_LINE ||
208            numberType == PhoneNumberType.MOBILE ||
209            numberType == PhoneNumberType.FIXED_LINE_OR_MOBILE);
210  }
211}
212