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.Phonenumber.PhoneNumber; 21 22 import java.io.IOException; 23 import java.io.InputStream; 24 import java.io.ObjectInputStream; 25 import java.util.HashMap; 26 import java.util.Locale; 27 import java.util.Map; 28 import java.util.logging.Level; 29 import java.util.logging.Logger; 30 31 /** 32 * An offline geocoder which provides geographical information related to a phone number. 33 * 34 * @author Shaopeng Jia 35 */ 36 public class PhoneNumberOfflineGeocoder { 37 private static PhoneNumberOfflineGeocoder instance = null; 38 private static final String MAPPING_DATA_DIRECTORY = 39 "/com/android/i18n/phonenumbers/geocoding/data/"; 40 private static final Logger LOGGER = Logger.getLogger(PhoneNumberOfflineGeocoder.class.getName()); 41 42 private final PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance(); 43 private final String phonePrefixDataDirectory; 44 45 // The mappingFileProvider knows for which combination of countryCallingCode and language a phone 46 // prefix mapping file is available in the file system, so that a file can be loaded when needed. 47 private MappingFileProvider mappingFileProvider = new MappingFileProvider(); 48 49 // A mapping from countryCallingCode_lang to the corresponding phone prefix map that has been 50 // loaded. 51 private Map<String, AreaCodeMap> availablePhonePrefixMaps = new HashMap<String, AreaCodeMap>(); 52 53 // @VisibleForTesting 54 PhoneNumberOfflineGeocoder(String phonePrefixDataDirectory) { 55 this.phonePrefixDataDirectory = phonePrefixDataDirectory; 56 loadMappingFileProvider(); 57 } 58 59 private void loadMappingFileProvider() { 60 InputStream source = 61 PhoneNumberOfflineGeocoder.class.getResourceAsStream(phonePrefixDataDirectory + "config"); 62 ObjectInputStream in = null; 63 try { 64 in = new ObjectInputStream(source); 65 mappingFileProvider.readExternal(in); 66 } catch (IOException e) { 67 LOGGER.log(Level.WARNING, e.toString()); 68 } finally { 69 close(in); 70 } 71 } 72 73 private AreaCodeMap getPhonePrefixDescriptions( 74 int prefixMapKey, String language, String script, String region) { 75 String fileName = mappingFileProvider.getFileName(prefixMapKey, language, script, region); 76 if (fileName.length() == 0) { 77 return null; 78 } 79 if (!availablePhonePrefixMaps.containsKey(fileName)) { 80 loadAreaCodeMapFromFile(fileName); 81 } 82 return availablePhonePrefixMaps.get(fileName); 83 } 84 85 private void loadAreaCodeMapFromFile(String fileName) { 86 InputStream source = 87 PhoneNumberOfflineGeocoder.class.getResourceAsStream(phonePrefixDataDirectory + fileName); 88 ObjectInputStream in = null; 89 try { 90 in = new ObjectInputStream(source); 91 AreaCodeMap map = new AreaCodeMap(); 92 map.readExternal(in); 93 availablePhonePrefixMaps.put(fileName, map); 94 } catch (IOException e) { 95 LOGGER.log(Level.WARNING, e.toString()); 96 } finally { 97 close(in); 98 } 99 } 100 101 private static void close(InputStream in) { 102 if (in != null) { 103 try { 104 in.close(); 105 } catch (IOException e) { 106 LOGGER.log(Level.WARNING, e.toString()); 107 } 108 } 109 } 110 111 /** 112 * Gets a {@link PhoneNumberOfflineGeocoder} instance to carry out international phone number 113 * geocoding. 114 * 115 * <p> The {@link PhoneNumberOfflineGeocoder} is implemented as a singleton. Therefore, calling 116 * this method multiple times will only result in one instance being created. 117 * 118 * @return a {@link PhoneNumberOfflineGeocoder} instance 119 */ 120 public static synchronized PhoneNumberOfflineGeocoder getInstance() { 121 if (instance == null) { 122 instance = new PhoneNumberOfflineGeocoder(MAPPING_DATA_DIRECTORY); 123 } 124 return instance; 125 } 126 127 /** 128 * Returns the customary display name in the given language for the given territory the phone 129 * number is from. 130 */ 131 private String getCountryNameForNumber(PhoneNumber number, Locale language) { 132 String regionCode = phoneUtil.getRegionCodeForNumber(number); 133 return getRegionDisplayName(regionCode, language); 134 } 135 136 /** 137 * Returns the customary display name in the given language for the given region. 138 */ 139 private String getRegionDisplayName(String regionCode, Locale language) { 140 return (regionCode == null || regionCode.equals("ZZ") || 141 regionCode.equals(PhoneNumberUtil.REGION_CODE_FOR_NON_GEO_ENTITY)) 142 ? "" : new Locale("", regionCode).getDisplayCountry(language); 143 } 144 145 /** 146 * Returns a text description for the given phone number, in the language provided. The 147 * description might consist of the name of the country where the phone number is from, or the 148 * name of the geographical area the phone number is from if more detailed information is 149 * available. 150 * 151 * <p>This method assumes the validity of the number passed in has already been checked. 152 * 153 * @param number a valid phone number for which we want to get a text description 154 * @param languageCode the language code for which the description should be written 155 * @return a text description for the given language code for the given phone number 156 */ 157 public String getDescriptionForValidNumber(PhoneNumber number, Locale languageCode) { 158 String langStr = languageCode.getLanguage(); 159 String scriptStr = ""; // No script is specified 160 String regionStr = languageCode.getCountry(); 161 162 String areaDescription = 163 getAreaDescriptionForNumber(number, langStr, scriptStr, regionStr); 164 return (areaDescription.length() > 0) 165 ? areaDescription : getCountryNameForNumber(number, languageCode); 166 } 167 168 /** 169 * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale)} but also considers the 170 * region of the user. If the phone number is from the same region as the user, only a lower-level 171 * description will be returned, if one exists. Otherwise, the phone number's region will be 172 * returned, with optionally some more detailed information. 173 * 174 * <p>For example, for a user from the region "US" (United States), we would show "Mountain View, 175 * CA" for a particular number, omitting the United States from the description. For a user from 176 * the United Kingdom (region "GB"), for the same number we may show "Mountain View, CA, United 177 * States" or even just "United States". 178 * 179 * <p>This method assumes the validity of the number passed in has already been checked. 180 * 181 * @param number the phone number for which we want to get a text description 182 * @param languageCode the language code for which the description should be written 183 * @param userRegion the region code for a given user. This region will be omitted from the 184 * description if the phone number comes from this region. It is a two-letter uppercase ISO 185 * country code as defined by ISO 3166-1. 186 * @return a text description for the given language code for the given phone number, or empty 187 * string if the number passed in is invalid 188 */ 189 public String getDescriptionForValidNumber(PhoneNumber number, Locale languageCode, 190 String userRegion) { 191 // If the user region matches the number's region, then we just show the lower-level 192 // description, if one exists - if no description exists, we will show the region(country) name 193 // for the number. 194 String regionCode = phoneUtil.getRegionCodeForNumber(number); 195 if (userRegion.equals(regionCode)) { 196 return getDescriptionForValidNumber(number, languageCode); 197 } 198 // Otherwise, we just show the region(country) name for now. 199 return getRegionDisplayName(regionCode, languageCode); 200 // TODO: Concatenate the lower-level and country-name information in an appropriate 201 // way for each language. 202 } 203 204 /** 205 * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale)} but explicitly checks 206 * the validity of the number passed in. 207 * 208 * @param number the phone number for which we want to get a text description 209 * @param languageCode the language code for which the description should be written 210 * @return a text description for the given language code for the given phone number, or empty 211 * string if the number passed in is invalid 212 */ 213 public String getDescriptionForNumber(PhoneNumber number, Locale languageCode) { 214 if (!phoneUtil.isValidNumber(number)) { 215 return ""; 216 } 217 return getDescriptionForValidNumber(number, languageCode); 218 } 219 220 /** 221 * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale, String)} but 222 * explicitly checks the validity of the number passed in. 223 * 224 * @param number the phone number for which we want to get a text description 225 * @param languageCode the language code for which the description should be written 226 * @param userRegion the region code for a given user. This region will be omitted from the 227 * description if the phone number comes from this region. It is a two-letter uppercase ISO 228 * country code as defined by ISO 3166-1. 229 * @return a text description for the given language code for the given phone number, or empty 230 * string if the number passed in is invalid 231 */ 232 public String getDescriptionForNumber(PhoneNumber number, Locale languageCode, 233 String userRegion) { 234 if (!phoneUtil.isValidNumber(number)) { 235 return ""; 236 } 237 return getDescriptionForValidNumber(number, languageCode, userRegion); 238 } 239 240 /** 241 * Returns an area-level text description in the given language for the given phone number. 242 * 243 * @param number the phone number for which we want to get a text description 244 * @param lang two-letter lowercase ISO language codes as defined by ISO 639-1 245 * @param script four-letter titlecase (the first letter is uppercase and the rest of the letters 246 * are lowercase) ISO script codes as defined in ISO 15924 247 * @param region two-letter uppercase ISO country codes as defined by ISO 3166-1 248 * @return an area-level text description in the given language for the given phone number, or an 249 * empty string if such a description is not available 250 */ 251 private String getAreaDescriptionForNumber( 252 PhoneNumber number, String lang, String script, String region) { 253 int countryCallingCode = number.getCountryCode(); 254 // As the NANPA data is split into multiple files covering 3-digit areas, use a phone number 255 // prefix of 4 digits for NANPA instead, e.g. 1650. 256 int phonePrefix = (countryCallingCode != 1) ? 257 countryCallingCode : (1000 + (int) (number.getNationalNumber() / 10000000)); 258 AreaCodeMap phonePrefixDescriptions = 259 getPhonePrefixDescriptions(phonePrefix, lang, script, region); 260 String description = (phonePrefixDescriptions != null) 261 ? phonePrefixDescriptions.lookup(number) 262 : null; 263 // When a location is not available in the requested language, fall back to English. 264 if ((description == null || description.length() == 0) && mayFallBackToEnglish(lang)) { 265 AreaCodeMap defaultMap = getPhonePrefixDescriptions(phonePrefix, "en", "", ""); 266 if (defaultMap == null) { 267 return ""; 268 } 269 description = defaultMap.lookup(number); 270 } 271 return description != null ? description : ""; 272 } 273 274 private boolean mayFallBackToEnglish(String lang) { 275 // Don't fall back to English if the requested language is among the following: 276 // - Chinese 277 // - Japanese 278 // - Korean 279 return !lang.equals("zh") && !lang.equals("ja") && !lang.equals("ko"); 280 } 281 } 282