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