• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
PhoneNumberOfflineGeocoder(String phonePrefixDataDirectory)54   PhoneNumberOfflineGeocoder(String phonePrefixDataDirectory) {
55     this.phonePrefixDataDirectory = phonePrefixDataDirectory;
56     loadMappingFileProvider();
57   }
58 
loadMappingFileProvider()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 
getPhonePrefixDescriptions( int prefixMapKey, String language, String script, String region)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 
loadAreaCodeMapFromFile(String fileName)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 
close(InputStream in)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    */
getInstance()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    */
getCountryNameForNumber(PhoneNumber number, Locale language)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    */
getRegionDisplayName(String regionCode, Locale language)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    */
getDescriptionForValidNumber(PhoneNumber number, Locale languageCode)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    */
getDescriptionForValidNumber(PhoneNumber number, Locale languageCode, String userRegion)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    */
getDescriptionForNumber(PhoneNumber number, Locale languageCode)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    */
getDescriptionForNumber(PhoneNumber number, Locale languageCode, String userRegion)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    */
getAreaDescriptionForNumber( PhoneNumber number, String lang, String script, String region)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 
mayFallBackToEnglish(String lang)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