2 * Copyright (C) 2011 The Libphonenumber Authors
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.google.i18n.phonenumbers.geocoding;
19 import com.google.i18n.phonenumbers.PhoneNumberUtil;
20 import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberType;
21 import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.io.ObjectInputStream;
26 import java.util.HashMap;
27 import java.util.Locale;
29 import java.util.logging.Level;
30 import java.util.logging.Logger;
33 * An offline geocoder which provides geographical information related to a phone number.
35 * @author Shaopeng Jia
37 public class PhoneNumberOfflineGeocoder {
38 private static PhoneNumberOfflineGeocoder instance = null;
39 private static final String MAPPING_DATA_DIRECTORY =
40 "/com/google/i18n/phonenumbers/geocoding/data/";
41 private static final Logger LOGGER = Logger.getLogger(PhoneNumberOfflineGeocoder.class.getName());
43 private final PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
44 private final String phonePrefixDataDirectory;
46 // The mappingFileProvider knows for which combination of countryCallingCode and language a phone
47 // prefix mapping file is available in the file system, so that a file can be loaded when needed.
48 private MappingFileProvider mappingFileProvider = new MappingFileProvider();
50 // A mapping from countryCallingCode_lang to the corresponding phone prefix map that has been
52 private Map<String, AreaCodeMap> availablePhonePrefixMaps = new HashMap<String, AreaCodeMap>();
55 PhoneNumberOfflineGeocoder(String phonePrefixDataDirectory) {
56 this.phonePrefixDataDirectory = phonePrefixDataDirectory;
57 loadMappingFileProvider();
60 private void loadMappingFileProvider() {
62 PhoneNumberOfflineGeocoder.class.getResourceAsStream(phonePrefixDataDirectory + "config");
63 ObjectInputStream in = null;
65 in = new ObjectInputStream(source);
66 mappingFileProvider.readExternal(in);
67 } catch (IOException e) {
68 LOGGER.log(Level.WARNING, e.toString());
74 private AreaCodeMap getPhonePrefixDescriptions(
75 int prefixMapKey, String language, String script, String region) {
76 String fileName = mappingFileProvider.getFileName(prefixMapKey, language, script, region);
77 if (fileName.length() == 0) {
80 if (!availablePhonePrefixMaps.containsKey(fileName)) {
81 loadAreaCodeMapFromFile(fileName);
83 return availablePhonePrefixMaps.get(fileName);
86 private void loadAreaCodeMapFromFile(String fileName) {
88 PhoneNumberOfflineGeocoder.class.getResourceAsStream(phonePrefixDataDirectory + fileName);
89 ObjectInputStream in = null;
91 in = new ObjectInputStream(source);
92 AreaCodeMap map = new AreaCodeMap();
94 availablePhonePrefixMaps.put(fileName, map);
95 } catch (IOException e) {
96 LOGGER.log(Level.WARNING, e.toString());
102 private static void close(InputStream in) {
106 } catch (IOException e) {
107 LOGGER.log(Level.WARNING, e.toString());
113 * Gets a {@link PhoneNumberOfflineGeocoder} instance to carry out international phone number
116 * <p> The {@link PhoneNumberOfflineGeocoder} is implemented as a singleton. Therefore, calling
117 * this method multiple times will only result in one instance being created.
119 * @return a {@link PhoneNumberOfflineGeocoder} instance
121 public static synchronized PhoneNumberOfflineGeocoder getInstance() {
122 if (instance == null) {
123 instance = new PhoneNumberOfflineGeocoder(MAPPING_DATA_DIRECTORY);
129 * Returns the customary display name in the given language for the given territory the phone
132 private String getCountryNameForNumber(PhoneNumber number, Locale language) {
133 String regionCode = phoneUtil.getRegionCodeForNumber(number);
134 return getRegionDisplayName(regionCode, language);
138 * Returns the customary display name in the given language for the given region.
140 private String getRegionDisplayName(String regionCode, Locale language) {
141 return (regionCode == null || regionCode.equals("ZZ") ||
142 regionCode.equals(PhoneNumberUtil.REGION_CODE_FOR_NON_GEO_ENTITY))
143 ? "" : new Locale("", regionCode).getDisplayCountry(language);
147 * Returns a text description for the given phone number, in the language provided. The
148 * description might consist of the name of the country where the phone number is from, or the
149 * name of the geographical area the phone number is from if more detailed information is
152 * <p>This method assumes the validity of the number passed in has already been checked, and that
153 * the number is suitable for geocoding. We consider fixed-line and mobile numbers possible
154 * candidates for geocoding.
156 * @param number a valid phone number for which we want to get a text description
157 * @param languageCode the language code for which the description should be written
158 * @return a text description for the given language code for the given phone number
160 public String getDescriptionForValidNumber(PhoneNumber number, Locale languageCode) {
161 String langStr = languageCode.getLanguage();
162 String scriptStr = ""; // No script is specified
163 String regionStr = languageCode.getCountry();
165 String areaDescription =
166 getAreaDescriptionForNumber(number, langStr, scriptStr, regionStr);
167 return (areaDescription.length() > 0)
168 ? areaDescription : getCountryNameForNumber(number, languageCode);
172 * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale)} but also considers the
173 * region of the user. If the phone number is from the same region as the user, only a lower-level
174 * description will be returned, if one exists. Otherwise, the phone number's region will be
175 * returned, with optionally some more detailed information.
177 * <p>For example, for a user from the region "US" (United States), we would show "Mountain View,
178 * CA" for a particular number, omitting the United States from the description. For a user from
179 * the United Kingdom (region "GB"), for the same number we may show "Mountain View, CA, United
180 * States" or even just "United States".
182 * <p>This method assumes the validity of the number passed in has already been checked.
184 * @param number the phone number for which we want to get a text description
185 * @param languageCode the language code for which the description should be written
186 * @param userRegion the region code for a given user. This region will be omitted from the
187 * description if the phone number comes from this region. It is a two-letter uppercase ISO
188 * country code as defined by ISO 3166-1.
189 * @return a text description for the given language code for the given phone number, or empty
190 * string if the number passed in is invalid
192 public String getDescriptionForValidNumber(PhoneNumber number, Locale languageCode,
194 // If the user region matches the number's region, then we just show the lower-level
195 // description, if one exists - if no description exists, we will show the region(country) name
197 String regionCode = phoneUtil.getRegionCodeForNumber(number);
198 if (userRegion.equals(regionCode)) {
199 return getDescriptionForValidNumber(number, languageCode);
201 // Otherwise, we just show the region(country) name for now.
202 return getRegionDisplayName(regionCode, languageCode);
203 // TODO: Concatenate the lower-level and country-name information in an appropriate
204 // way for each language.
208 * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale)} but explicitly checks
209 * the validity of the number passed in.
211 * @param number the phone number for which we want to get a text description
212 * @param languageCode the language code for which the description should be written
213 * @return a text description for the given language code for the given phone number, or empty
214 * string if the number passed in is invalid
216 public String getDescriptionForNumber(PhoneNumber number, Locale languageCode) {
217 PhoneNumberType numberType = phoneUtil.getNumberType(number);
218 if (numberType == PhoneNumberType.UNKNOWN) {
220 } else if (!canBeGeocoded(numberType)) {
221 return getCountryNameForNumber(number, languageCode);
223 return getDescriptionForValidNumber(number, languageCode);
227 * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale, String)} but
228 * explicitly checks the validity of the number passed in.
230 * @param number the phone number for which we want to get a text description
231 * @param languageCode the language code for which the description should be written
232 * @param userRegion the region code for a given user. This region will be omitted from the
233 * description if the phone number comes from this region. It is a two-letter uppercase ISO
234 * country code as defined by ISO 3166-1.
235 * @return a text description for the given language code for the given phone number, or empty
236 * string if the number passed in is invalid
238 public String getDescriptionForNumber(PhoneNumber number, Locale languageCode,
240 PhoneNumberType numberType = phoneUtil.getNumberType(number);
241 if (numberType == PhoneNumberType.UNKNOWN) {
243 } else if (!canBeGeocoded(numberType)) {
244 return getCountryNameForNumber(number, languageCode);
246 return getDescriptionForValidNumber(number, languageCode, userRegion);
250 * A similar method is implemented as PhoneNumberUtil.isNumberGeographical, which performs a
251 * stricter check, as it determines if a number has a geographical association. Also, if new
252 * phone number types were added, we should check if this other method should be updated too.
254 private boolean canBeGeocoded(PhoneNumberType numberType) {
255 return (numberType == PhoneNumberType.FIXED_LINE ||
256 numberType == PhoneNumberType.MOBILE ||
257 numberType == PhoneNumberType.FIXED_LINE_OR_MOBILE);
261 * Returns an area-level text description in the given language for the given phone number.
263 * @param number the phone number for which we want to get a text description
264 * @param lang two-letter lowercase ISO language codes as defined by ISO 639-1
265 * @param script four-letter titlecase (the first letter is uppercase and the rest of the letters
266 * are lowercase) ISO script codes as defined in ISO 15924
267 * @param region two-letter uppercase ISO country codes as defined by ISO 3166-1
268 * @return an area-level text description in the given language for the given phone number, or an
269 * empty string if such a description is not available
271 private String getAreaDescriptionForNumber(
272 PhoneNumber number, String lang, String script, String region) {
273 int countryCallingCode = number.getCountryCode();
274 // As the NANPA data is split into multiple files covering 3-digit areas, use a phone number
275 // prefix of 4 digits for NANPA instead, e.g. 1650.
276 int phonePrefix = (countryCallingCode != 1) ?
277 countryCallingCode : (1000 + (int) (number.getNationalNumber() / 10000000));
278 AreaCodeMap phonePrefixDescriptions =
279 getPhonePrefixDescriptions(phonePrefix, lang, script, region);
280 String description = (phonePrefixDescriptions != null)
281 ? phonePrefixDescriptions.lookup(number)
283 // When a location is not available in the requested language, fall back to English.
284 if ((description == null || description.length() == 0) && mayFallBackToEnglish(lang)) {
285 AreaCodeMap defaultMap = getPhonePrefixDescriptions(phonePrefix, "en", "", "");
286 if (defaultMap == null) {
289 description = defaultMap.lookup(number);
291 return description != null ? description : "";
294 private boolean mayFallBackToEnglish(String lang) {
295 // Don't fall back to English if the requested language is among the following:
299 return !lang.equals("zh") && !lang.equals("ja") && !lang.equals("ko");