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.google.i18n.phonenumbers.geocoding; 18 19 import com.google.i18n.phonenumbers.NumberParseException; 20 import com.google.i18n.phonenumbers.PhoneNumberUtil; 21 import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberType; 22 import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; 23 import com.google.i18n.phonenumbers.prefixmapper.PrefixFileReader; 24 25 import java.util.List; 26 import java.util.Locale; 27 28 /** 29 * An offline geocoder which provides geographical information related to a phone number. 30 * 31 * @author Shaopeng Jia 32 */ 33 public class PhoneNumberOfflineGeocoder { 34 private static PhoneNumberOfflineGeocoder instance = null; 35 private static final String MAPPING_DATA_DIRECTORY = 36 "/com/google/i18n/phonenumbers/geocoding/data/"; 37 private PrefixFileReader prefixFileReader = null; 38 39 private final PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance(); 40 41 // @VisibleForTesting PhoneNumberOfflineGeocoder(String phonePrefixDataDirectory)42 PhoneNumberOfflineGeocoder(String phonePrefixDataDirectory) { 43 prefixFileReader = new PrefixFileReader(phonePrefixDataDirectory); 44 } 45 46 /** 47 * Gets a {@link PhoneNumberOfflineGeocoder} instance to carry out international phone number 48 * geocoding. 49 * 50 * <p> The {@link PhoneNumberOfflineGeocoder} is implemented as a singleton. Therefore, calling 51 * this method multiple times will only result in one instance being created. 52 * 53 * @return a {@link PhoneNumberOfflineGeocoder} instance 54 */ getInstance()55 public static synchronized PhoneNumberOfflineGeocoder getInstance() { 56 if (instance == null) { 57 instance = new PhoneNumberOfflineGeocoder(MAPPING_DATA_DIRECTORY); 58 } 59 return instance; 60 } 61 62 /** 63 * Returns the customary display name in the given language for the given territory the phone 64 * number is from. If it could be from many territories, nothing is returned. 65 */ getCountryNameForNumber(PhoneNumber number, Locale language)66 private String getCountryNameForNumber(PhoneNumber number, Locale language) { 67 List<String> regionCodes = 68 phoneUtil.getRegionCodesForCountryCode(number.getCountryCode()); 69 if (regionCodes.size() == 1) { 70 return getRegionDisplayName(regionCodes.get(0), language); 71 } else { 72 String regionWhereNumberIsValid = "ZZ"; 73 for (String regionCode : regionCodes) { 74 if (phoneUtil.isValidNumberForRegion(number, regionCode)) { 75 if (!regionWhereNumberIsValid.equals("ZZ")) { 76 // If we can't assign the phone number as definitely belonging to only one territory, 77 // then we return nothing. 78 return ""; 79 } 80 regionWhereNumberIsValid = regionCode; 81 } 82 } 83 return getRegionDisplayName(regionWhereNumberIsValid, language); 84 } 85 } 86 87 /** 88 * Returns the customary display name in the given language for the given region. 89 */ getRegionDisplayName(String regionCode, Locale language)90 private String getRegionDisplayName(String regionCode, Locale language) { 91 return (regionCode == null || regionCode.equals("ZZ") || 92 regionCode.equals(PhoneNumberUtil.REGION_CODE_FOR_NON_GEO_ENTITY)) 93 ? "" : new Locale("", regionCode).getDisplayCountry(language); 94 } 95 96 /** 97 * Returns a text description for the given phone number, in the language provided. The 98 * description might consist of the name of the country where the phone number is from, or the 99 * name of the geographical area the phone number is from if more detailed information is 100 * available. 101 * 102 * <p>This method assumes the validity of the number passed in has already been checked, and that 103 * the number is suitable for geocoding. We consider fixed-line and mobile numbers possible 104 * candidates for geocoding. 105 * 106 * @param number a valid phone number for which we want to get a text description 107 * @param languageCode the language code for which the description should be written 108 * @return a text description for the given language code for the given phone number 109 */ getDescriptionForValidNumber(PhoneNumber number, Locale languageCode)110 public String getDescriptionForValidNumber(PhoneNumber number, Locale languageCode) { 111 String langStr = languageCode.getLanguage(); 112 String scriptStr = ""; // No script is specified 113 String regionStr = languageCode.getCountry(); 114 115 String areaDescription; 116 String mobileToken = PhoneNumberUtil.getCountryMobileToken(number.getCountryCode()); 117 String nationalNumber = phoneUtil.getNationalSignificantNumber(number); 118 if (!mobileToken.equals("") && nationalNumber.startsWith(mobileToken)) { 119 // In some countries, eg. Argentina, mobile numbers have a mobile token before the national 120 // destination code, this should be removed before geocoding. 121 nationalNumber = nationalNumber.substring(mobileToken.length()); 122 String region = phoneUtil.getRegionCodeForCountryCode(number.getCountryCode()); 123 PhoneNumber copiedNumber; 124 try { 125 copiedNumber = phoneUtil.parse(nationalNumber, region); 126 } catch (NumberParseException e) { 127 // If this happens, just reuse what we had. 128 copiedNumber = number; 129 } 130 areaDescription = prefixFileReader.getDescriptionForNumber(copiedNumber, langStr, scriptStr, 131 regionStr); 132 } else { 133 areaDescription = prefixFileReader.getDescriptionForNumber(number, langStr, scriptStr, 134 regionStr); 135 } 136 return (areaDescription.length() > 0) 137 ? areaDescription : getCountryNameForNumber(number, languageCode); 138 } 139 140 /** 141 * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale)} but also considers the 142 * region of the user. If the phone number is from the same region as the user, only a lower-level 143 * description will be returned, if one exists. Otherwise, the phone number's region will be 144 * returned, with optionally some more detailed information. 145 * 146 * <p>For example, for a user from the region "US" (United States), we would show "Mountain View, 147 * CA" for a particular number, omitting the United States from the description. For a user from 148 * the United Kingdom (region "GB"), for the same number we may show "Mountain View, CA, United 149 * States" or even just "United States". 150 * 151 * <p>This method assumes the validity of the number passed in has already been checked. 152 * 153 * @param number the 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 * @param userRegion the region code for a given user. This region will be omitted from the 156 * description if the phone number comes from this region. It is a two-letter uppercase ISO 157 * country code as defined by ISO 3166-1. 158 * @return a text description for the given language code for the given phone number, or empty 159 * string if the number passed in is invalid 160 */ getDescriptionForValidNumber(PhoneNumber number, Locale languageCode, String userRegion)161 public String getDescriptionForValidNumber(PhoneNumber number, Locale languageCode, 162 String userRegion) { 163 // If the user region matches the number's region, then we just show the lower-level 164 // description, if one exists - if no description exists, we will show the region(country) name 165 // for the number. 166 String regionCode = phoneUtil.getRegionCodeForNumber(number); 167 if (userRegion.equals(regionCode)) { 168 return getDescriptionForValidNumber(number, languageCode); 169 } 170 // Otherwise, we just show the region(country) name for now. 171 return getRegionDisplayName(regionCode, languageCode); 172 // TODO: Concatenate the lower-level and country-name information in an appropriate 173 // way for each language. 174 } 175 176 /** 177 * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale)} but explicitly checks 178 * the validity of the number passed in. 179 * 180 * @param number the phone number for which we want to get a text description 181 * @param languageCode the language code for which the description should be written 182 * @return a text description for the given language code for the given phone number, or empty 183 * string if the number passed in is invalid 184 */ getDescriptionForNumber(PhoneNumber number, Locale languageCode)185 public String getDescriptionForNumber(PhoneNumber number, Locale languageCode) { 186 PhoneNumberType numberType = phoneUtil.getNumberType(number); 187 if (numberType == PhoneNumberType.UNKNOWN) { 188 return ""; 189 } else if (!canBeGeocoded(numberType)) { 190 return getCountryNameForNumber(number, languageCode); 191 } 192 return getDescriptionForValidNumber(number, languageCode); 193 } 194 195 /** 196 * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale, String)} but 197 * explicitly checks the validity of the number passed in. 198 * 199 * @param number the phone number for which we want to get a text description 200 * @param languageCode the language code for which the description should be written 201 * @param userRegion the region code for a given user. This region will be omitted from the 202 * description if the phone number comes from this region. It is a two-letter uppercase ISO 203 * country code as defined by ISO 3166-1. 204 * @return a text description for the given language code for the given phone number, or empty 205 * string if the number passed in is invalid 206 */ getDescriptionForNumber(PhoneNumber number, Locale languageCode, String userRegion)207 public String getDescriptionForNumber(PhoneNumber number, Locale languageCode, 208 String userRegion) { 209 PhoneNumberType numberType = phoneUtil.getNumberType(number); 210 if (numberType == PhoneNumberType.UNKNOWN) { 211 return ""; 212 } else if (!canBeGeocoded(numberType)) { 213 return getCountryNameForNumber(number, languageCode); 214 } 215 return getDescriptionForValidNumber(number, languageCode, userRegion); 216 } 217 218 /** 219 * A similar method is implemented as PhoneNumberUtil.isNumberGeographical, which performs a 220 * stricter check, as it determines if a number has a geographical association. Also, if new 221 * phone number types were added, we should check if this other method should be updated too. 222 */ canBeGeocoded(PhoneNumberType numberType)223 private boolean canBeGeocoded(PhoneNumberType numberType) { 224 return (numberType == PhoneNumberType.FIXED_LINE || 225 numberType == PhoneNumberType.MOBILE || 226 numberType == PhoneNumberType.FIXED_LINE_OR_MOBILE); 227 } 228 } 229