Home | History | Annotate | Download | only in geocoding
      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.PhoneNumberUtil.PhoneNumberType;
     21 import com.android.i18n.phonenumbers.Phonenumber.PhoneNumber;
     22 
     23 import java.io.IOException;
     24 import java.io.InputStream;
     25 import java.io.ObjectInputStream;
     26 import java.util.HashMap;
     27 import java.util.Locale;
     28 import java.util.Map;
     29 import java.util.logging.Level;
     30 import java.util.logging.Logger;
     31 
     32 /**
     33  * An offline geocoder which provides geographical information related to a phone number.
     34  *
     35  * @author Shaopeng Jia
     36  */
     37 public class PhoneNumberOfflineGeocoder {
     38   private static PhoneNumberOfflineGeocoder instance = null;
     39   private static final String MAPPING_DATA_DIRECTORY =
     40       "/com/android/i18n/phonenumbers/geocoding/data/";
     41   private static final Logger LOGGER = Logger.getLogger(PhoneNumberOfflineGeocoder.class.getName());
     42 
     43   private final PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
     44   private final String phonePrefixDataDirectory;
     45 
     46   // The mappingFileProvider knows for which combination of countryCallingCode and language a phone
     47   // prefix mapping file is available in the file system, so that a file can be loaded when needed.
     48   private MappingFileProvider mappingFileProvider = new MappingFileProvider();
     49 
     50   // A mapping from countryCallingCode_lang to the corresponding phone prefix map that has been
     51   // loaded.
     52   private Map<String, AreaCodeMap> availablePhonePrefixMaps = new HashMap<String, AreaCodeMap>();
     53 
     54   // @VisibleForTesting
     55   PhoneNumberOfflineGeocoder(String phonePrefixDataDirectory) {
     56     this.phonePrefixDataDirectory = phonePrefixDataDirectory;
     57     loadMappingFileProvider();
     58   }
     59 
     60   private void loadMappingFileProvider() {
     61     InputStream source =
     62         PhoneNumberOfflineGeocoder.class.getResourceAsStream(phonePrefixDataDirectory + "config");
     63     ObjectInputStream in = null;
     64     try {
     65       in = new ObjectInputStream(source);
     66       mappingFileProvider.readExternal(in);
     67     } catch (IOException e) {
     68       LOGGER.log(Level.WARNING, e.toString());
     69     } finally {
     70       close(in);
     71     }
     72   }
     73 
     74   private AreaCodeMap getPhonePrefixDescriptions(
     75       int prefixMapKey, String language, String script, String region) {
     76     String fileName = mappingFileProvider.getFileName(prefixMapKey, language, script, region);
     77     if (fileName.length() == 0) {
     78       return null;
     79     }
     80     if (!availablePhonePrefixMaps.containsKey(fileName)) {
     81       loadAreaCodeMapFromFile(fileName);
     82     }
     83     return availablePhonePrefixMaps.get(fileName);
     84   }
     85 
     86   private void loadAreaCodeMapFromFile(String fileName) {
     87     InputStream source =
     88         PhoneNumberOfflineGeocoder.class.getResourceAsStream(phonePrefixDataDirectory + fileName);
     89     ObjectInputStream in = null;
     90     try {
     91       in = new ObjectInputStream(source);
     92       AreaCodeMap map = new AreaCodeMap();
     93       map.readExternal(in);
     94       availablePhonePrefixMaps.put(fileName, map);
     95     } catch (IOException e) {
     96       LOGGER.log(Level.WARNING, e.toString());
     97     } finally {
     98       close(in);
     99     }
    100   }
    101 
    102   private static void close(InputStream in) {
    103     if (in != null) {
    104       try {
    105         in.close();
    106       } catch (IOException e) {
    107         LOGGER.log(Level.WARNING, e.toString());
    108       }
    109     }
    110   }
    111 
    112   /**
    113    * Gets a {@link PhoneNumberOfflineGeocoder} instance to carry out international phone number
    114    * geocoding.
    115    *
    116    * <p> The {@link PhoneNumberOfflineGeocoder} is implemented as a singleton. Therefore, calling
    117    * this method multiple times will only result in one instance being created.
    118    *
    119    * @return  a {@link PhoneNumberOfflineGeocoder} instance
    120    */
    121   public static synchronized PhoneNumberOfflineGeocoder getInstance() {
    122     if (instance == null) {
    123       instance = new PhoneNumberOfflineGeocoder(MAPPING_DATA_DIRECTORY);
    124     }
    125     return instance;
    126   }
    127 
    128   /**
    129    * Returns the customary display name in the given language for the given territory the phone
    130    * number is from.
    131    */
    132   private String getCountryNameForNumber(PhoneNumber number, Locale language) {
    133     String regionCode = phoneUtil.getRegionCodeForNumber(number);
    134     return getRegionDisplayName(regionCode, language);
    135   }
    136 
    137   /**
    138    * Returns the customary display name in the given language for the given region.
    139    */
    140   private String getRegionDisplayName(String regionCode, Locale language) {
    141     return (regionCode == null || regionCode.equals("ZZ") ||
    142             regionCode.equals(PhoneNumberUtil.REGION_CODE_FOR_NON_GEO_ENTITY))
    143         ? "" : new Locale("", regionCode).getDisplayCountry(language);
    144   }
    145 
    146   /**
    147    * Returns a text description for the given phone number, in the language provided. The
    148    * description might consist of the name of the country where the phone number is from, or the
    149    * name of the geographical area the phone number is from if more detailed information is
    150    * available.
    151    *
    152    * <p>This method assumes the validity of the number passed in has already been checked, and that
    153    * the number is suitable for geocoding. We consider fixed-line and mobile numbers possible
    154    * candidates for geocoding.
    155    *
    156    * @param number  a valid phone number for which we want to get a text description
    157    * @param languageCode  the language code for which the description should be written
    158    * @return  a text description for the given language code for the given phone number
    159    */
    160   public String getDescriptionForValidNumber(PhoneNumber number, Locale languageCode) {
    161     String langStr = languageCode.getLanguage();
    162     String scriptStr = "";  // No script is specified
    163     String regionStr = languageCode.getCountry();
    164 
    165     String areaDescription =
    166         getAreaDescriptionForNumber(number, langStr, scriptStr, regionStr);
    167     return (areaDescription.length() > 0)
    168         ? areaDescription : getCountryNameForNumber(number, languageCode);
    169   }
    170 
    171   /**
    172    * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale)} but also considers the
    173    * region of the user. If the phone number is from the same region as the user, only a lower-level
    174    * description will be returned, if one exists. Otherwise, the phone number's region will be
    175    * returned, with optionally some more detailed information.
    176    *
    177    * <p>For example, for a user from the region "US" (United States), we would show "Mountain View,
    178    * CA" for a particular number, omitting the United States from the description. For a user from
    179    * the United Kingdom (region "GB"), for the same number we may show "Mountain View, CA, United
    180    * States" or even just "United States".
    181    *
    182    * <p>This method assumes the validity of the number passed in has already been checked.
    183    *
    184    * @param number  the phone number for which we want to get a text description
    185    * @param languageCode  the language code for which the description should be written
    186    * @param userRegion  the region code for a given user. This region will be omitted from the
    187    *     description if the phone number comes from this region. It is a two-letter uppercase ISO
    188    *     country code as defined by ISO 3166-1.
    189    * @return  a text description for the given language code for the given phone number, or empty
    190    *     string if the number passed in is invalid
    191    */
    192   public String getDescriptionForValidNumber(PhoneNumber number, Locale languageCode,
    193                                              String userRegion) {
    194     // If the user region matches the number's region, then we just show the lower-level
    195     // description, if one exists - if no description exists, we will show the region(country) name
    196     // for the number.
    197     String regionCode = phoneUtil.getRegionCodeForNumber(number);
    198     if (userRegion.equals(regionCode)) {
    199       return getDescriptionForValidNumber(number, languageCode);
    200     }
    201     // Otherwise, we just show the region(country) name for now.
    202     return getRegionDisplayName(regionCode, languageCode);
    203     // TODO: Concatenate the lower-level and country-name information in an appropriate
    204     // way for each language.
    205   }
    206 
    207   /**
    208    * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale)} but explicitly checks
    209    * the validity of the number passed in.
    210    *
    211    * @param number  the phone number for which we want to get a text description
    212    * @param languageCode  the language code for which the description should be written
    213    * @return  a text description for the given language code for the given phone number, or empty
    214    *     string if the number passed in is invalid
    215    */
    216   public String getDescriptionForNumber(PhoneNumber number, Locale languageCode) {
    217     PhoneNumberType numberType = phoneUtil.getNumberType(number);
    218     if (numberType == PhoneNumberType.UNKNOWN) {
    219       return "";
    220     } else if (!canBeGeocoded(numberType)) {
    221       return getCountryNameForNumber(number, languageCode);
    222     }
    223     return getDescriptionForValidNumber(number, languageCode);
    224   }
    225 
    226   /**
    227    * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale, String)} but
    228    * explicitly checks the validity of the number passed in.
    229    *
    230    * @param number  the phone number for which we want to get a text description
    231    * @param languageCode  the language code for which the description should be written
    232    * @param userRegion  the region code for a given user. This region will be omitted from the
    233    *     description if the phone number comes from this region. It is a two-letter uppercase ISO
    234    *     country code as defined by ISO 3166-1.
    235    * @return  a text description for the given language code for the given phone number, or empty
    236    *     string if the number passed in is invalid
    237    */
    238   public String getDescriptionForNumber(PhoneNumber number, Locale languageCode,
    239                                         String userRegion) {
    240     PhoneNumberType numberType = phoneUtil.getNumberType(number);
    241     if (numberType == PhoneNumberType.UNKNOWN) {
    242       return "";
    243     } else if (!canBeGeocoded(numberType)) {
    244       return getCountryNameForNumber(number, languageCode);
    245     }
    246     return getDescriptionForValidNumber(number, languageCode, userRegion);
    247   }
    248 
    249   /**
    250    * A similar method is implemented as PhoneNumberUtil.isNumberGeographical, which performs a
    251    * stricter check, as it determines if a number has a geographical association. Also, if new
    252    * phone number types were added, we should check if this other method should be updated too.
    253    */
    254   private boolean canBeGeocoded(PhoneNumberType numberType) {
    255     return (numberType == PhoneNumberType.FIXED_LINE ||
    256             numberType == PhoneNumberType.MOBILE ||
    257             numberType == PhoneNumberType.FIXED_LINE_OR_MOBILE);
    258   }
    259 
    260   /**
    261    * Returns an area-level text description in the given language for the given phone number.
    262    *
    263    * @param number  the phone number for which we want to get a text description
    264    * @param lang  two-letter lowercase ISO language codes as defined by ISO 639-1
    265    * @param script  four-letter titlecase (the first letter is uppercase and the rest of the letters
    266    *     are lowercase) ISO script codes as defined in ISO 15924
    267    * @param region  two-letter uppercase ISO country codes as defined by ISO 3166-1
    268    * @return  an area-level text description in the given language for the given phone number, or an
    269    *     empty string if such a description is not available
    270    */
    271   private String getAreaDescriptionForNumber(
    272       PhoneNumber number, String lang, String script, String region) {
    273     int countryCallingCode = number.getCountryCode();
    274     // As the NANPA data is split into multiple files covering 3-digit areas, use a phone number
    275     // prefix of 4 digits for NANPA instead, e.g. 1650.
    276     int phonePrefix = (countryCallingCode != 1) ?
    277         countryCallingCode : (1000 + (int) (number.getNationalNumber() / 10000000));
    278     AreaCodeMap phonePrefixDescriptions =
    279         getPhonePrefixDescriptions(phonePrefix, lang, script, region);
    280     String description = (phonePrefixDescriptions != null)
    281         ? phonePrefixDescriptions.lookup(number)
    282         : null;
    283     // When a location is not available in the requested language, fall back to English.
    284     if ((description == null || description.length() == 0) && mayFallBackToEnglish(lang)) {
    285       AreaCodeMap defaultMap = getPhonePrefixDescriptions(phonePrefix, "en", "", "");
    286       if (defaultMap == null) {
    287         return "";
    288       }
    289       description = defaultMap.lookup(number);
    290     }
    291     return description != null ? description : "";
    292   }
    293 
    294   private boolean mayFallBackToEnglish(String lang) {
    295     // Don't fall back to English if the requested language is among the following:
    296     // - Chinese
    297     // - Japanese
    298     // - Korean
    299     return !lang.equals("zh") && !lang.equals("ja") && !lang.equals("ko");
    300   }
    301 }
    302