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