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 17 package com.android.i18n.phonenumbers.geocoding; 18 19 import com.android.i18n.phonenumbers.PhoneNumberUtil; 20 import com.android.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberType; 21 import com.android.i18n.phonenumbers.Phonenumber.PhoneNumber; 22 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; 28 import java.util.Map; 29 import java.util.logging.Level; 30 import java.util.logging.Logger; 31 32 /** 33 * An offline geocoder which provides geographical information related to a phone number. 34 * 35 * @author Shaopeng Jia 36 */ 37 public class PhoneNumberOfflineGeocoder { 38 private static PhoneNumberOfflineGeocoder instance = null; 39 private static final String MAPPING_DATA_DIRECTORY = 40 "/com/android/i18n/phonenumbers/geocoding/data/"; 41 private static final Logger LOGGER = Logger.getLogger(PhoneNumberOfflineGeocoder.class.getName()); 42 43 private final PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance(); 44 private final String phonePrefixDataDirectory; 45 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(); 49 50 // A mapping from countryCallingCode_lang to the corresponding phone prefix map that has been 51 // loaded. 52 private Map<String, AreaCodeMap> availablePhonePrefixMaps = new HashMap<String, AreaCodeMap>(); 53 54 // @VisibleForTesting 55 PhoneNumberOfflineGeocoder(String phonePrefixDataDirectory) { 56 this.phonePrefixDataDirectory = phonePrefixDataDirectory; 57 loadMappingFileProvider(); 58 } 59 60 private void loadMappingFileProvider() { 61 InputStream source = 62 PhoneNumberOfflineGeocoder.class.getResourceAsStream(phonePrefixDataDirectory + "config"); 63 ObjectInputStream in = null; 64 try { 65 in = new ObjectInputStream(source); 66 mappingFileProvider.readExternal(in); 67 } catch (IOException e) { 68 LOGGER.log(Level.WARNING, e.toString()); 69 } finally { 70 close(in); 71 } 72 } 73 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) { 78 return null; 79 } 80 if (!availablePhonePrefixMaps.containsKey(fileName)) { 81 loadAreaCodeMapFromFile(fileName); 82 } 83 return availablePhonePrefixMaps.get(fileName); 84 } 85 86 private void loadAreaCodeMapFromFile(String fileName) { 87 InputStream source = 88 PhoneNumberOfflineGeocoder.class.getResourceAsStream(phonePrefixDataDirectory + fileName); 89 ObjectInputStream in = null; 90 try { 91 in = new ObjectInputStream(source); 92 AreaCodeMap map = new AreaCodeMap(); 93 map.readExternal(in); 94 availablePhonePrefixMaps.put(fileName, map); 95 } catch (IOException e) { 96 LOGGER.log(Level.WARNING, e.toString()); 97 } finally { 98 close(in); 99 } 100 } 101 102 private static void close(InputStream in) { 103 if (in != null) { 104 try { 105 in.close(); 106 } catch (IOException e) { 107 LOGGER.log(Level.WARNING, e.toString()); 108 } 109 } 110 } 111 112 /** 113 * Gets a {@link PhoneNumberOfflineGeocoder} instance to carry out international phone number 114 * geocoding. 115 * 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. 118 * 119 * @return a {@link PhoneNumberOfflineGeocoder} instance 120 */ 121 public static synchronized PhoneNumberOfflineGeocoder getInstance() { 122 if (instance == null) { 123 instance = new PhoneNumberOfflineGeocoder(MAPPING_DATA_DIRECTORY); 124 } 125 return instance; 126 } 127 128 /** 129 * Returns the customary display name in the given language for the given territory the phone 130 * number is from. 131 */ 132 private String getCountryNameForNumber(PhoneNumber number, Locale language) { 133 String regionCode = phoneUtil.getRegionCodeForNumber(number); 134 return getRegionDisplayName(regionCode, language); 135 } 136 137 /** 138 * Returns the customary display name in the given language for the given region. 139 */ 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); 144 } 145 146 /** 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 150 * available. 151 * 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. 155 * 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 159 */ 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(); 164 165 String areaDescription = 166 getAreaDescriptionForNumber(number, langStr, scriptStr, regionStr); 167 return (areaDescription.length() > 0) 168 ? areaDescription : getCountryNameForNumber(number, languageCode); 169 } 170 171 /** 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. 176 * 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". 181 * 182 * <p>This method assumes the validity of the number passed in has already been checked. 183 * 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 191 */ 192 public String getDescriptionForValidNumber(PhoneNumber number, Locale languageCode, 193 String userRegion) { 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 196 // for the number. 197 String regionCode = phoneUtil.getRegionCodeForNumber(number); 198 if (userRegion.equals(regionCode)) { 199 return getDescriptionForValidNumber(number, languageCode); 200 } 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. 205 } 206 207 /** 208 * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale)} but explicitly checks 209 * the validity of the number passed in. 210 * 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 215 */ 216 public String getDescriptionForNumber(PhoneNumber number, Locale languageCode) { 217 PhoneNumberType numberType = phoneUtil.getNumberType(number); 218 if (numberType == PhoneNumberType.UNKNOWN) { 219 return ""; 220 } else if (!canBeGeocoded(numberType)) { 221 return getCountryNameForNumber(number, languageCode); 222 } 223 return getDescriptionForValidNumber(number, languageCode); 224 } 225 226 /** 227 * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale, String)} but 228 * explicitly checks the validity of the number passed in. 229 * 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 237 */ 238 public String getDescriptionForNumber(PhoneNumber number, Locale languageCode, 239 String userRegion) { 240 PhoneNumberType numberType = phoneUtil.getNumberType(number); 241 if (numberType == PhoneNumberType.UNKNOWN) { 242 return ""; 243 } else if (!canBeGeocoded(numberType)) { 244 return getCountryNameForNumber(number, languageCode); 245 } 246 return getDescriptionForValidNumber(number, languageCode, userRegion); 247 } 248 249 /** 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. 253 */ 254 private boolean canBeGeocoded(PhoneNumberType numberType) { 255 return (numberType == PhoneNumberType.FIXED_LINE || 256 numberType == PhoneNumberType.MOBILE || 257 numberType == PhoneNumberType.FIXED_LINE_OR_MOBILE); 258 } 259 260 /** 261 * Returns an area-level text description in the given language for the given phone number. 262 * 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 270 */ 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) 282 : null; 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) { 287 return ""; 288 } 289 description = defaultMap.lookup(number); 290 } 291 return description != null ? description : ""; 292 } 293 294 private boolean mayFallBackToEnglish(String lang) { 295 // Don't fall back to English if the requested language is among the following: 296 // - Chinese 297 // - Japanese 298 // - Korean 299 return !lang.equals("zh") && !lang.equals("ja") && !lang.equals("ko"); 300 } 301 } 302