1 /* 2 * Copyright (C) 2011 Google Inc. 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 countryCallingCode, String language, String script, String region) { 75 String fileName = mappingFileProvider.getFileName(countryCallingCode, 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 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 (regionCode == null || regionCode.equals("ZZ")) 134 ? "" : new Locale("", regionCode).getDisplayCountry(language); 135 } 136 137 /** 138 * Returns a text description for the given language code for the given phone number. The 139 * description might consist of the name of the country where the phone number is from and/or the 140 * name of the geographical area the phone number is from. This method assumes the validity of the 141 * number passed in has already been checked. 142 * 143 * @param number a valid phone number for which we want to get a text description 144 * @param languageCode the language code for which the description should be written 145 * @return a text description for the given language code for the given phone number 146 */ 147 public String getDescriptionForValidNumber(PhoneNumber number, Locale languageCode) { 148 String langStr = languageCode.getLanguage(); 149 String scriptStr = ""; // No script is specified 150 String regionStr = languageCode.getCountry(); 151 152 String areaDescription = 153 getAreaDescriptionForNumber(number, langStr, scriptStr, regionStr); 154 return (areaDescription.length() > 0) 155 ? areaDescription : getCountryNameForNumber(number, languageCode); 156 } 157 158 /** 159 * Returns a text description for the given language code for the given phone number. The 160 * description might consist of the name of the country where the phone number is from and/or the 161 * name of the geographical area the phone number is from. This method explictly checkes the 162 * validity of the number passed in. 163 * 164 * @param number the phone number for which we want to get a text description 165 * @param languageCode the language code for which the description should be written 166 * @return a text description for the given language code for the given phone number, or empty 167 * string if the number passed in is invalid 168 */ 169 public String getDescriptionForNumber(PhoneNumber number, Locale languageCode) { 170 if (!phoneUtil.isValidNumber(number)) { 171 return ""; 172 } 173 return getDescriptionForValidNumber(number, languageCode); 174 } 175 176 /** 177 * Returns an area-level text description in the given language for the given phone number. 178 * 179 * @param number the phone number for which we want to get a text description 180 * @param lang two-letter lowercase ISO language codes as defined by ISO 639-1 181 * @param script four-letter titlecase (the first letter is uppercase and the rest of the letters 182 * are lowercase) ISO script codes as defined in ISO 15924 183 * @param region two-letter uppercase ISO country codes as defined by ISO 3166-1 184 * @return an area-level text description in the given language for the given phone number, or an 185 * empty string if such a description is not available 186 */ 187 private String getAreaDescriptionForNumber( 188 PhoneNumber number, String lang, String script, String region) { 189 int countryCallingCode = number.getCountryCode(); 190 // As the NANPA data is split into multiple files covering 3-digit areas, use a phone number 191 // prefix of 4 digits for NANPA instead, e.g. 1650. 192 int phonePrefix = (countryCallingCode != 1) ? 193 countryCallingCode : (1000 + (int) (number.getNationalNumber() / 10000000)); 194 AreaCodeMap phonePrefixDescriptions = 195 getPhonePrefixDescriptions(phonePrefix, lang, script, region); 196 return (phonePrefixDescriptions != null) ? phonePrefixDescriptions.lookup(number) : ""; 197 } 198 } 199