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.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