Home | History | Annotate | Download | only in phonenumberutil
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      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.dialer.phonenumberutil;
     18 
     19 import android.content.Context;
     20 import android.database.Cursor;
     21 import android.net.Uri;
     22 import android.os.Trace;
     23 import android.provider.CallLog;
     24 import android.support.annotation.NonNull;
     25 import android.support.annotation.Nullable;
     26 import android.telecom.PhoneAccountHandle;
     27 import android.telephony.PhoneNumberUtils;
     28 import android.telephony.TelephonyManager;
     29 import android.text.BidiFormatter;
     30 import android.text.TextDirectionHeuristics;
     31 import android.text.TextUtils;
     32 import com.android.dialer.common.Assert;
     33 import com.android.dialer.common.LogUtil;
     34 import com.android.dialer.compat.CompatUtils;
     35 import com.android.dialer.compat.telephony.TelephonyManagerCompat;
     36 import com.android.dialer.phonenumbergeoutil.PhoneNumberGeoUtilComponent;
     37 import com.android.dialer.telecom.TelecomUtil;
     38 import com.google.common.base.Ascii;
     39 import java.util.Arrays;
     40 import java.util.HashSet;
     41 import java.util.Set;
     42 
     43 public class PhoneNumberHelper {
     44 
     45   private static final String TAG = "PhoneNumberUtil";
     46   private static final Set<String> LEGACY_UNKNOWN_NUMBERS =
     47       new HashSet<>(Arrays.asList("-1", "-2", "-3"));
     48 
     49   /** Returns true if it is possible to place a call to the given number. */
     50   public static boolean canPlaceCallsTo(CharSequence number, int presentation) {
     51     return presentation == CallLog.Calls.PRESENTATION_ALLOWED
     52         && !TextUtils.isEmpty(number)
     53         && !isLegacyUnknownNumbers(number);
     54   }
     55 
     56   /**
     57    * Move the given cursor to a position where the number it points to matches the number in a
     58    * contact lookup URI.
     59    *
     60    * <p>We assume the cursor is one returned by the Contacts Provider when the URI asks for a
     61    * specific number. This method's behavior is undefined when the cursor doesn't meet the
     62    * assumption.
     63    *
     64    * <p>When determining whether two phone numbers are identical enough for caller ID purposes, the
     65    * Contacts Provider ignores special characters such as '#'. This makes it possible for the cursor
     66    * returned by the Contacts Provider to have multiple rows even when the URI asks for a specific
     67    * number.
     68    *
     69    * <p>For example, suppose the user has two contacts whose numbers are "#123" and "123",
     70    * respectively. When the URI asks for number "123", both numbers will be returned. Therefore, the
     71    * following strategy is employed to find a match.
     72    *
     73    * <p>In the following description, we use E to denote a number the cursor points to (an existing
     74    * contact number), and L to denote the number in the contact lookup URI.
     75    *
     76    * <p>If neither E nor L contains special characters, return true to indicate a match is found.
     77    *
     78    * <p>If either E or L contains special characters, return true when the raw numbers of E and L
     79    * are the same. Otherwise, move the cursor to its next position and start over.
     80    *
     81    * <p>Return false in all other circumstances to indicate that no match can be found.
     82    *
     83    * <p>When no match can be found, the cursor is after the last result when the method returns.
     84    *
     85    * @param cursor A cursor returned by the Contacts Provider.
     86    * @param columnIndexForNumber The index of the column where phone numbers are stored. It is the
     87    *     caller's responsibility to pass the correct column index.
     88    * @param contactLookupUri A URI used to retrieve a contact via the Contacts Provider. It is the
     89    *     caller's responsibility to ensure the URI is one that asks for a specific phone number.
     90    * @return true if a match can be found.
     91    */
     92   public static boolean updateCursorToMatchContactLookupUri(
     93       @Nullable Cursor cursor, int columnIndexForNumber, @Nullable Uri contactLookupUri) {
     94     if (cursor == null || contactLookupUri == null) {
     95       return false;
     96     }
     97 
     98     if (!cursor.moveToFirst()) {
     99       return false;
    100     }
    101 
    102     Assert.checkArgument(
    103         0 <= columnIndexForNumber && columnIndexForNumber < cursor.getColumnCount());
    104 
    105     String lookupNumber = contactLookupUri.getLastPathSegment();
    106     if (TextUtils.isEmpty(lookupNumber)) {
    107       return false;
    108     }
    109 
    110     boolean lookupNumberHasSpecialChars = numberHasSpecialChars(lookupNumber);
    111 
    112     do {
    113       String existingContactNumber = cursor.getString(columnIndexForNumber);
    114       boolean existingContactNumberHasSpecialChars = numberHasSpecialChars(existingContactNumber);
    115 
    116       if ((!lookupNumberHasSpecialChars && !existingContactNumberHasSpecialChars)
    117           || sameRawNumbers(existingContactNumber, lookupNumber)) {
    118         return true;
    119       }
    120 
    121     } while (cursor.moveToNext());
    122 
    123     return false;
    124   }
    125 
    126   /** Returns true if the input phone number contains special characters. */
    127   public static boolean numberHasSpecialChars(String number) {
    128     return !TextUtils.isEmpty(number) && number.contains("#");
    129   }
    130 
    131   /** Returns true if the raw numbers of the two input phone numbers are the same. */
    132   public static boolean sameRawNumbers(String number1, String number2) {
    133     String rawNumber1 =
    134         PhoneNumberUtils.stripSeparators(PhoneNumberUtils.convertKeypadLettersToDigits(number1));
    135     String rawNumber2 =
    136         PhoneNumberUtils.stripSeparators(PhoneNumberUtils.convertKeypadLettersToDigits(number2));
    137 
    138     return rawNumber1.equals(rawNumber2);
    139   }
    140 
    141   /**
    142    * Returns true if the given number is the number of the configured voicemail. To be able to
    143    * mock-out this, it is not a static method.
    144    */
    145   public static boolean isVoicemailNumber(
    146       Context context, PhoneAccountHandle accountHandle, CharSequence number) {
    147     if (TextUtils.isEmpty(number)) {
    148       return false;
    149     }
    150     return TelecomUtil.isVoicemailNumber(context, accountHandle, number.toString());
    151   }
    152 
    153   /**
    154    * Returns true if the given number is a SIP address. To be able to mock-out this, it is not a
    155    * static method.
    156    */
    157   public static boolean isSipNumber(CharSequence number) {
    158     return number != null && isUriNumber(number.toString());
    159   }
    160 
    161   public static boolean isUnknownNumberThatCanBeLookedUp(
    162       Context context, PhoneAccountHandle accountHandle, CharSequence number, int presentation) {
    163     if (presentation == CallLog.Calls.PRESENTATION_UNKNOWN) {
    164       return false;
    165     }
    166     if (presentation == CallLog.Calls.PRESENTATION_RESTRICTED) {
    167       return false;
    168     }
    169     if (presentation == CallLog.Calls.PRESENTATION_PAYPHONE) {
    170       return false;
    171     }
    172     if (TextUtils.isEmpty(number)) {
    173       return false;
    174     }
    175     if (isVoicemailNumber(context, accountHandle, number)) {
    176       return false;
    177     }
    178     if (isLegacyUnknownNumbers(number)) {
    179       return false;
    180     }
    181     return true;
    182   }
    183 
    184   public static boolean isLegacyUnknownNumbers(CharSequence number) {
    185     return number != null && LEGACY_UNKNOWN_NUMBERS.contains(number.toString());
    186   }
    187 
    188   /**
    189    * @param countryIso Country ISO used if there is no country code in the number, may be null
    190    *     otherwise.
    191    * @return a geographical description string for the specified number.
    192    */
    193   public static String getGeoDescription(
    194       Context context, String number, @Nullable String countryIso) {
    195     return PhoneNumberGeoUtilComponent.get(context)
    196         .getPhoneNumberGeoUtil()
    197         .getGeoDescription(context, number, countryIso);
    198   }
    199 
    200   /**
    201    * @param phoneAccountHandle {@code PhonAccountHandle} used to get current network country ISO.
    202    *     May be null if no account is in use or selected, in which case default account will be
    203    *     used.
    204    * @return The ISO 3166-1 two letters country code of the country the user is in based on the
    205    *     network location. If the network location does not exist, fall back to the locale setting.
    206    */
    207   public static String getCurrentCountryIso(
    208       Context context, @Nullable PhoneAccountHandle phoneAccountHandle) {
    209     Trace.beginSection("PhoneNumberHelper.getCurrentCountryIso");
    210     // Without framework function calls, this seems to be the most accurate location service
    211     // we can rely on.
    212     String countryIso =
    213         TelephonyManagerCompat.getNetworkCountryIsoForPhoneAccountHandle(
    214             context, phoneAccountHandle);
    215     if (TextUtils.isEmpty(countryIso)) {
    216       countryIso = CompatUtils.getLocale(context).getCountry();
    217       LogUtil.i(
    218           "PhoneNumberHelper.getCurrentCountryIso",
    219           "No CountryDetector; falling back to countryIso based on locale: " + countryIso);
    220     }
    221     countryIso = countryIso.toUpperCase();
    222     Trace.endSection();
    223 
    224     return countryIso;
    225   }
    226 
    227   /**
    228    * An enhanced version of {@link PhoneNumberUtils#formatNumber(String, String, String)}.
    229    *
    230    * <p>The {@link Context} parameter allows us to tweak formatting according to device properties.
    231    *
    232    * <p>Returns the formatted phone number (e.g, 1-123-456-7890) or the original number if
    233    * formatting fails or is intentionally ignored.
    234    */
    235   public static String formatNumber(
    236       Context context, @Nullable String number, @Nullable String numberE164, String countryIso) {
    237     // The number can be null e.g. schema is voicemail and uri content is empty.
    238     if (number == null) {
    239       return null;
    240     }
    241 
    242     // Argentina phone number formats are complex and PhoneNumberUtils doesn't format all Argentina
    243     // numbers correctly.
    244     // To ensure consistent user experience, we disable phone number formatting for all numbers
    245     // (not just Argentinian ones) for devices with Argentinian SIMs.
    246     TelephonyManager telephonyManager =
    247         (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
    248     if (telephonyManager != null
    249         && "AR".equals(Ascii.toUpperCase(telephonyManager.getSimCountryIso()))) {
    250       return number;
    251     }
    252 
    253     String formattedNumber = PhoneNumberUtils.formatNumber(number, numberE164, countryIso);
    254     return formattedNumber != null ? formattedNumber : number;
    255   }
    256 
    257   /** @see #formatNumber(Context, String, String, String). */
    258   public static String formatNumber(Context context, @Nullable String number, String countryIso) {
    259     return formatNumber(context, number, /* numberE164 = */ null, countryIso);
    260   }
    261 
    262   @Nullable
    263   public static CharSequence formatNumberForDisplay(
    264       Context context, @Nullable String number, @NonNull String countryIso) {
    265     if (number == null) {
    266       return null;
    267     }
    268 
    269     return PhoneNumberUtils.createTtsSpannable(
    270         BidiFormatter.getInstance()
    271             .unicodeWrap(formatNumber(context, number, countryIso), TextDirectionHeuristics.LTR));
    272   }
    273 
    274   /**
    275    * Determines if the specified number is actually a URI (i.e. a SIP address) rather than a regular
    276    * PSTN phone number, based on whether or not the number contains an "@" character.
    277    *
    278    * @param number Phone number
    279    * @return true if number contains @
    280    *     <p>TODO: Remove if PhoneNumberUtils.isUriNumber(String number) is made public.
    281    */
    282   public static boolean isUriNumber(String number) {
    283     // Note we allow either "@" or "%40" to indicate a URI, in case
    284     // the passed-in string is URI-escaped.  (Neither "@" nor "%40"
    285     // will ever be found in a legal PSTN number.)
    286     return number != null && (number.contains("@") || number.contains("%40"));
    287   }
    288 
    289   /**
    290    * @param number SIP address of the form "username@domainname" (or the URI-escaped equivalent
    291    *     "username%40domainname")
    292    *     <p>TODO: Remove if PhoneNumberUtils.getUsernameFromUriNumber(String number) is made public.
    293    * @return the "username" part of the specified SIP address, i.e. the part before the "@"
    294    *     character (or "%40").
    295    */
    296   public static String getUsernameFromUriNumber(String number) {
    297     // The delimiter between username and domain name can be
    298     // either "@" or "%40" (the URI-escaped equivalent.)
    299     int delimiterIndex = number.indexOf('@');
    300     if (delimiterIndex < 0) {
    301       delimiterIndex = number.indexOf("%40");
    302     }
    303     if (delimiterIndex < 0) {
    304       LogUtil.i(
    305           "PhoneNumberHelper.getUsernameFromUriNumber",
    306           "getUsernameFromUriNumber: no delimiter found in SIP address: "
    307               + LogUtil.sanitizePii(number));
    308       return number;
    309     }
    310     return number.substring(0, delimiterIndex);
    311   }
    312 
    313   private static boolean isVerizon(Context context) {
    314     // Verizon MCC/MNC codes copied from com/android/voicemailomtp/res/xml/vvm_config.xml.
    315     // TODO(sail): Need a better way to do per carrier and per OEM configurations.
    316     switch (context.getSystemService(TelephonyManager.class).getSimOperator()) {
    317       case "310004":
    318       case "310010":
    319       case "310012":
    320       case "310013":
    321       case "310590":
    322       case "310890":
    323       case "310910":
    324       case "311110":
    325       case "311270":
    326       case "311271":
    327       case "311272":
    328       case "311273":
    329       case "311274":
    330       case "311275":
    331       case "311276":
    332       case "311277":
    333       case "311278":
    334       case "311279":
    335       case "311280":
    336       case "311281":
    337       case "311282":
    338       case "311283":
    339       case "311284":
    340       case "311285":
    341       case "311286":
    342       case "311287":
    343       case "311288":
    344       case "311289":
    345       case "311390":
    346       case "311480":
    347       case "311481":
    348       case "311482":
    349       case "311483":
    350       case "311484":
    351       case "311485":
    352       case "311486":
    353       case "311487":
    354       case "311488":
    355       case "311489":
    356         return true;
    357       default:
    358         return false;
    359     }
    360   }
    361 
    362   /**
    363    * Gets the label to display for a phone call where the presentation is set as
    364    * PRESENTATION_RESTRICTED. For Verizon we want this to be displayed as "Restricted". For all
    365    * other carriers we want this to be be displayed as "Private number".
    366    */
    367   public static String getDisplayNameForRestrictedNumber(Context context) {
    368     if (isVerizon(context)) {
    369       return context.getString(R.string.private_num_verizon);
    370     } else {
    371       return context.getString(R.string.private_num_non_verizon);
    372     }
    373   }
    374 }
    375