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; 63 try { 64 in = new ObjectInputStream(source); 65 mappingFileProvider.readExternal(in); 66 } catch (IOException e) { 67 LOGGER.log(Level.WARNING, e.toString()); 68 } 69 } 70 71 private AreaCodeMap getPhonePrefixDescriptions( 72 int countryCallingCode, String language, String script, String region) { 73 String fileName = mappingFileProvider.getFileName(countryCallingCode, language, script, region); 74 if (fileName.length() == 0) { 75 return null; 76 } 77 if (!availablePhonePrefixMaps.containsKey(fileName)) { 78 loadAreaCodeMapFromFile(fileName); 79 } 80 return availablePhonePrefixMaps.get(fileName); 81 } 82 83 private void loadAreaCodeMapFromFile(String fileName) { 84 InputStream source = 85 PhoneNumberOfflineGeocoder.class.getResourceAsStream(phonePrefixDataDirectory + fileName); 86 ObjectInputStream in; 87 try { 88 in = new ObjectInputStream(source); 89 AreaCodeMap map = new AreaCodeMap(); 90 map.readExternal(in); 91 availablePhonePrefixMaps.put(fileName, map); 92 } catch (IOException e) { 93 LOGGER.log(Level.WARNING, e.toString()); 94 } 95 } 96 97 /** 98 * Gets a {@link PhoneNumberOfflineGeocoder} instance to carry out international phone number 99 * geocoding. 100 * 101 * <p> The {@link PhoneNumberOfflineGeocoder} is implemented as a singleton. Therefore, calling 102 * this method multiple times will only result in one instance being created. 103 * 104 * @return a {@link PhoneNumberOfflineGeocoder} instance 105 */ 106 public static synchronized PhoneNumberOfflineGeocoder getInstance() { 107 if (instance == null) { 108 instance = new PhoneNumberOfflineGeocoder(MAPPING_DATA_DIRECTORY); 109 } 110 return instance; 111 } 112 113 /** 114 * Returns the customary display name in the given language for the given territory the phone 115 * number is from. 116 */ 117 private String getCountryNameForNumber(PhoneNumber number, Locale language) { 118 String regionCode = phoneUtil.getRegionCodeForNumber(number); 119 return (regionCode == null || regionCode.equals("ZZ")) 120 ? "" : new Locale("", regionCode).getDisplayCountry(language); 121 } 122 123 /** 124 * Returns a text description for the given language code for the given phone number. The 125 * description might consist of the name of the country where the phone number is from and/or the 126 * name of the geographical area the phone number is from. This method assumes the validity of the 127 * number passed in has already been checked. 128 * 129 * @param number a valid phone number for which we want to get a text description 130 * @param languageCode the language code for which the description should be written 131 * @return a text description for the given language code for the given phone number 132 */ 133 public String getDescriptionForValidNumber(PhoneNumber number, Locale languageCode) { 134 String langStr = languageCode.getLanguage(); 135 String scriptStr = ""; // No script is specified 136 String regionStr = languageCode.getCountry(); 137 138 String areaDescription = 139 getAreaDescriptionForNumber(number, langStr, scriptStr, regionStr); 140 return (areaDescription.length() > 0) 141 ? areaDescription : getCountryNameForNumber(number, languageCode); 142 } 143 144 /** 145 * Returns a text description for the given language code for the given phone number. The 146 * description might consist of the name of the country where the phone number is from and/or the 147 * name of the geographical area the phone number is from. This method explictly checkes the 148 * validity of the number passed in. 149 * 150 * @param number the phone number for which we want to get a text description 151 * @param languageCode the language code for which the description should be written 152 * @return a text description for the given language code for the given phone number, or empty 153 * string if the number passed in is invalid 154 */ 155 public String getDescriptionForNumber(PhoneNumber number, Locale languageCode) { 156 if (!phoneUtil.isValidNumber(number)) { 157 return ""; 158 } 159 return getDescriptionForValidNumber(number, languageCode); 160 } 161 162 /** 163 * Returns an area-level text description in the given language for the given phone number. 164 * 165 * @param number the phone number for which we want to get a text description 166 * @param lang two-letter lowercase ISO language codes as defined by ISO 639-1 167 * @param script four-letter titlecase (the first letter is uppercase and the rest of the letters 168 * are lowercase) ISO script codes as defined in ISO 15924 169 * @param region two-letter uppercase ISO country codes as defined by ISO 3166-1 170 * @return an area-level text description in the given language for the given phone number, or an 171 * empty string if such a description is not available 172 */ 173 private String getAreaDescriptionForNumber( 174 PhoneNumber number, String lang, String script, String region) { 175 int countryCallingCode = number.getCountryCode(); 176 // As the NANPA data is split into multiple files covering 3-digit areas, use a phone number 177 // prefix of 4 digits for NANPA instead, e.g. 1650. 178 int phonePrefix = (countryCallingCode != 1) ? 179 countryCallingCode : (1000 + (int) (number.getNationalNumber() / 10000000)); 180 AreaCodeMap phonePrefixDescriptions = 181 getPhonePrefixDescriptions(phonePrefix, lang, script, region); 182 return (phonePrefixDescriptions != null) ? phonePrefixDescriptions.lookup(number) : ""; 183 } 184 } 185