• 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.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 the number has already been found valid for one region, then we don't know which
76           // region it belongs to so we return nothing.
77           if (!regionWhereNumberIsValid.equals("ZZ")) {
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, or an
109    *     empty string if the number could come from multiple countries, or the country code is
110    *     in fact invalid
111    */
getDescriptionForValidNumber(PhoneNumber number, Locale languageCode)112   public String getDescriptionForValidNumber(PhoneNumber number, Locale languageCode) {
113     String langStr = languageCode.getLanguage();
114     String scriptStr = "";  // No script is specified
115     String regionStr = languageCode.getCountry();
116 
117     String areaDescription;
118     String mobileToken = PhoneNumberUtil.getCountryMobileToken(number.getCountryCode());
119     String nationalNumber = phoneUtil.getNationalSignificantNumber(number);
120     if (!mobileToken.equals("") && nationalNumber.startsWith(mobileToken)) {
121       // In some countries, eg. Argentina, mobile numbers have a mobile token before the national
122       // destination code, this should be removed before geocoding.
123       nationalNumber = nationalNumber.substring(mobileToken.length());
124       String region = phoneUtil.getRegionCodeForCountryCode(number.getCountryCode());
125       PhoneNumber copiedNumber;
126       try {
127         copiedNumber = phoneUtil.parse(nationalNumber, region);
128       } catch (NumberParseException e) {
129         // If this happens, just reuse what we had.
130         copiedNumber = number;
131       }
132       areaDescription = prefixFileReader.getDescriptionForNumber(copiedNumber, langStr, scriptStr,
133                                                                  regionStr);
134     } else {
135       areaDescription = prefixFileReader.getDescriptionForNumber(number, langStr, scriptStr,
136                                                                  regionStr);
137     }
138     return (areaDescription.length() > 0)
139         ? areaDescription : getCountryNameForNumber(number, languageCode);
140   }
141 
142   /**
143    * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale)} but also considers the
144    * region of the user. If the phone number is from the same region as the user, only a lower-level
145    * description will be returned, if one exists. Otherwise, the phone number's region will be
146    * returned, with optionally some more detailed information.
147    *
148    * <p>For example, for a user from the region "US" (United States), we would show "Mountain View,
149    * CA" for a particular number, omitting the United States from the description. For a user from
150    * the United Kingdom (region "GB"), for the same number we may show "Mountain View, CA, United
151    * States" or even just "United States".
152    *
153    * <p>This method assumes the validity of the number passed in has already been checked.
154    *
155    * @param number  the phone number for which we want to get a text description
156    * @param languageCode  the language code for which the description should be written
157    * @param userRegion  the region code for a given user. This region will be omitted from the
158    *     description if the phone number comes from this region. It should be a two-letter
159    *     uppercase ISO country code as defined by ISO 3166-1.
160    * @return  a text description for the given language code for the given phone number, or an
161    *     empty string if the number could come from multiple countries, or the country code is
162    *     in fact invalid
163    */
getDescriptionForValidNumber(PhoneNumber number, Locale languageCode, String userRegion)164   public String getDescriptionForValidNumber(PhoneNumber number, Locale languageCode,
165                                              String userRegion) {
166     // If the user region matches the number's region, then we just show the lower-level
167     // description, if one exists - if no description exists, we will show the region(country) name
168     // for the number.
169     String regionCode = phoneUtil.getRegionCodeForNumber(number);
170     if (userRegion.equals(regionCode)) {
171       return getDescriptionForValidNumber(number, languageCode);
172     }
173     // Otherwise, we just show the region(country) name for now.
174     return getRegionDisplayName(regionCode, languageCode);
175     // TODO: Concatenate the lower-level and country-name information in an appropriate
176     // way for each language.
177   }
178 
179   /**
180    * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale)} but explicitly checks
181    * the validity of the number passed in.
182    *
183    * @param number  the phone number for which we want to get a text description
184    * @param languageCode  the language code for which the description should be written
185    * @return  a text description for the given language code for the given phone number, or empty
186    *     string if the number passed in is invalid or could belong to multiple countries
187    */
getDescriptionForNumber(PhoneNumber number, Locale languageCode)188   public String getDescriptionForNumber(PhoneNumber number, Locale languageCode) {
189     PhoneNumberType numberType = phoneUtil.getNumberType(number);
190     if (numberType == PhoneNumberType.UNKNOWN) {
191       return "";
192     } else if (!phoneUtil.isNumberGeographical(numberType, number.getCountryCode())) {
193       return getCountryNameForNumber(number, languageCode);
194     }
195     return getDescriptionForValidNumber(number, languageCode);
196   }
197 
198   /**
199    * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale, String)} but
200    * explicitly checks the validity of the number passed in.
201    *
202    * @param number  the phone number for which we want to get a text description
203    * @param languageCode  the language code for which the description should be written
204    * @param userRegion  the region code for a given user. This region will be omitted from the
205    *     description if the phone number comes from this region. It should be a two-letter
206    *     uppercase ISO country code as defined by ISO 3166-1.
207    * @return  a text description for the given language code for the given phone number, or empty
208    *     string if the number passed in is invalid or could belong to multiple countries
209    */
getDescriptionForNumber(PhoneNumber number, Locale languageCode, String userRegion)210   public String getDescriptionForNumber(PhoneNumber number, Locale languageCode,
211                                         String userRegion) {
212     PhoneNumberType numberType = phoneUtil.getNumberType(number);
213     if (numberType == PhoneNumberType.UNKNOWN) {
214       return "";
215     } else if (!phoneUtil.isNumberGeographical(numberType, number.getCountryCode())) {
216       return getCountryNameForNumber(number, languageCode);
217     }
218     return getDescriptionForValidNumber(number, languageCode, userRegion);
219   }
220 }
221