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