/* * Copyright (C) 2006 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.telephony; import com.android.i18n.phonenumbers.NumberParseException; import com.android.i18n.phonenumbers.PhoneNumberUtil; import com.android.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat; import com.android.i18n.phonenumbers.Phonenumber.PhoneNumber; import com.android.i18n.phonenumbers.ShortNumberInfo; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.database.Cursor; import android.location.CountryDetector; import android.net.Uri; import android.os.SystemProperties; import android.os.PersistableBundle; import android.provider.Contacts; import android.provider.ContactsContract; import android.telecom.PhoneAccount; import android.text.Editable; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.style.TtsSpan; import android.util.SparseIntArray; import static com.android.internal.telephony.TelephonyProperties.PROPERTY_OPERATOR_IDP_STRING; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Various utilities for dealing with phone number strings. */ public class PhoneNumberUtils { /* * Special characters * * (See "What is a phone number?" doc) * 'p' --- GSM pause character, same as comma * 'n' --- GSM wild character * 'w' --- GSM wait character */ public static final char PAUSE = ','; public static final char WAIT = ';'; public static final char WILD = 'N'; /* * Calling Line Identification Restriction (CLIR) */ private static final String CLIR_ON = "*31#"; private static final String CLIR_OFF = "#31#"; /* * TOA = TON + NPI * See TS 24.008 section 10.5.4.7 for details. * These are the only really useful TOA values */ public static final int TOA_International = 0x91; public static final int TOA_Unknown = 0x81; static final String LOG_TAG = "PhoneNumberUtils"; private static final boolean DBG = false; /* * global-phone-number = ["+"] 1*( DIGIT / written-sep ) * written-sep = ("-"/".") */ private static final Pattern GLOBAL_PHONE_NUMBER_PATTERN = Pattern.compile("[\\+]?[0-9.-]+"); /** True if c is ISO-LATIN characters 0-9 */ public static boolean isISODigit (char c) { return c >= '0' && c <= '9'; } /** True if c is ISO-LATIN characters 0-9, *, # */ public final static boolean is12Key(char c) { return (c >= '0' && c <= '9') || c == '*' || c == '#'; } /** True if c is ISO-LATIN characters 0-9, *, # , +, WILD */ public final static boolean isDialable(char c) { return (c >= '0' && c <= '9') || c == '*' || c == '#' || c == '+' || c == WILD; } /** True if c is ISO-LATIN characters 0-9, *, # , + (no WILD) */ public final static boolean isReallyDialable(char c) { return (c >= '0' && c <= '9') || c == '*' || c == '#' || c == '+'; } /** True if c is ISO-LATIN characters 0-9, *, # , +, WILD, WAIT, PAUSE */ public final static boolean isNonSeparator(char c) { return (c >= '0' && c <= '9') || c == '*' || c == '#' || c == '+' || c == WILD || c == WAIT || c == PAUSE; } /** This any anything to the right of this char is part of the * post-dial string (eg this is PAUSE or WAIT) */ public final static boolean isStartsPostDial (char c) { return c == PAUSE || c == WAIT; } private static boolean isPause (char c){ return c == 'p'||c == 'P'; } private static boolean isToneWait (char c){ return c == 'w'||c == 'W'; } /** Returns true if ch is not dialable or alpha char */ private static boolean isSeparator(char ch) { return !isDialable(ch) && !(('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z')); } /** Extracts the phone number from an Intent. * * @param intent the intent to get the number of * @param context a context to use for database access * * @return the phone number that would be called by the intent, or * null if the number cannot be found. */ public static String getNumberFromIntent(Intent intent, Context context) { String number = null; Uri uri = intent.getData(); if (uri == null) { return null; } String scheme = uri.getScheme(); if (scheme.equals("tel") || scheme.equals("sip")) { return uri.getSchemeSpecificPart(); } if (context == null) { return null; } String type = intent.resolveType(context); String phoneColumn = null; // Correctly read out the phone entry based on requested provider final String authority = uri.getAuthority(); if (Contacts.AUTHORITY.equals(authority)) { phoneColumn = Contacts.People.Phones.NUMBER; } else if (ContactsContract.AUTHORITY.equals(authority)) { phoneColumn = ContactsContract.CommonDataKinds.Phone.NUMBER; } Cursor c = null; try { c = context.getContentResolver().query(uri, new String[] { phoneColumn }, null, null, null); if (c != null) { if (c.moveToFirst()) { number = c.getString(c.getColumnIndex(phoneColumn)); } } } catch (RuntimeException e) { Rlog.e(LOG_TAG, "Error getting phone number.", e); } finally { if (c != null) { c.close(); } } return number; } /** Extracts the network address portion and canonicalizes * (filters out separators.) * Network address portion is everything up to DTMF control digit * separators (pause or wait), but without non-dialable characters. * * Please note that the GSM wild character is allowed in the result. * This must be resolved before dialing. * * Returns null if phoneNumber == null */ public static String extractNetworkPortion(String phoneNumber) { if (phoneNumber == null) { return null; } int len = phoneNumber.length(); StringBuilder ret = new StringBuilder(len); for (int i = 0; i < len; i++) { char c = phoneNumber.charAt(i); // Character.digit() supports ASCII and Unicode digits (fullwidth, Arabic-Indic, etc.) int digit = Character.digit(c, 10); if (digit != -1) { ret.append(digit); } else if (c == '+') { // Allow '+' as first character or after CLIR MMI prefix String prefix = ret.toString(); if (prefix.length() == 0 || prefix.equals(CLIR_ON) || prefix.equals(CLIR_OFF)) { ret.append(c); } } else if (isDialable(c)) { ret.append(c); } else if (isStartsPostDial (c)) { break; } } return ret.toString(); } /** * Extracts the network address portion and canonicalize. * * This function is equivalent to extractNetworkPortion(), except * for allowing the PLUS character to occur at arbitrary positions * in the address portion, not just the first position. * * @hide */ public static String extractNetworkPortionAlt(String phoneNumber) { if (phoneNumber == null) { return null; } int len = phoneNumber.length(); StringBuilder ret = new StringBuilder(len); boolean haveSeenPlus = false; for (int i = 0; i < len; i++) { char c = phoneNumber.charAt(i); if (c == '+') { if (haveSeenPlus) { continue; } haveSeenPlus = true; } if (isDialable(c)) { ret.append(c); } else if (isStartsPostDial (c)) { break; } } return ret.toString(); } /** * Strips separators from a phone number string. * @param phoneNumber phone number to strip. * @return phone string stripped of separators. */ public static String stripSeparators(String phoneNumber) { if (phoneNumber == null) { return null; } int len = phoneNumber.length(); StringBuilder ret = new StringBuilder(len); for (int i = 0; i < len; i++) { char c = phoneNumber.charAt(i); // Character.digit() supports ASCII and Unicode digits (fullwidth, Arabic-Indic, etc.) int digit = Character.digit(c, 10); if (digit != -1) { ret.append(digit); } else if (isNonSeparator(c)) { ret.append(c); } } return ret.toString(); } /** * Translates keypad letters to actual digits (e.g. 1-800-GOOG-411 will * become 1-800-4664-411), and then strips all separators (e.g. 1-800-4664-411 will become * 18004664411). * * @see #convertKeypadLettersToDigits(String) * @see #stripSeparators(String) * * @hide */ public static String convertAndStrip(String phoneNumber) { return stripSeparators(convertKeypadLettersToDigits(phoneNumber)); } /** * Converts pause and tonewait pause characters * to Android representation. * RFC 3601 says pause is 'p' and tonewait is 'w'. * @hide */ public static String convertPreDial(String phoneNumber) { if (phoneNumber == null) { return null; } int len = phoneNumber.length(); StringBuilder ret = new StringBuilder(len); for (int i = 0; i < len; i++) { char c = phoneNumber.charAt(i); if (isPause(c)) { c = PAUSE; } else if (isToneWait(c)) { c = WAIT; } ret.append(c); } return ret.toString(); } /** or -1 if both are negative */ static private int minPositive (int a, int b) { if (a >= 0 && b >= 0) { return (a < b) ? a : b; } else if (a >= 0) { /* && b < 0 */ return a; } else if (b >= 0) { /* && a < 0 */ return b; } else { /* a < 0 && b < 0 */ return -1; } } private static void log(String msg) { Rlog.d(LOG_TAG, msg); } /** index of the last character of the network portion * (eg anything after is a post-dial string) */ static private int indexOfLastNetworkChar(String a) { int pIndex, wIndex; int origLength; int trimIndex; origLength = a.length(); pIndex = a.indexOf(PAUSE); wIndex = a.indexOf(WAIT); trimIndex = minPositive(pIndex, wIndex); if (trimIndex < 0) { return origLength - 1; } else { return trimIndex - 1; } } /** * Extracts the post-dial sequence of DTMF control digits, pauses, and * waits. Strips separators. This string may be empty, but will not be null * unless phoneNumber == null. * * Returns null if phoneNumber == null */ public static String extractPostDialPortion(String phoneNumber) { if (phoneNumber == null) return null; int trimIndex; StringBuilder ret = new StringBuilder(); trimIndex = indexOfLastNetworkChar (phoneNumber); for (int i = trimIndex + 1, s = phoneNumber.length() ; i < s; i++ ) { char c = phoneNumber.charAt(i); if (isNonSeparator(c)) { ret.append(c); } } return ret.toString(); } /** * Compare phone numbers a and b, return true if they're identical enough for caller ID purposes. */ public static boolean compare(String a, String b) { // We've used loose comparation at least Eclair, which may change in the future. return compare(a, b, false); } /** * Compare phone numbers a and b, and return true if they're identical * enough for caller ID purposes. Checks a resource to determine whether * to use a strict or loose comparison algorithm. */ public static boolean compare(Context context, String a, String b) { boolean useStrict = context.getResources().getBoolean( com.android.internal.R.bool.config_use_strict_phone_number_comparation); return compare(a, b, useStrict); } /** * @hide only for testing. */ public static boolean compare(String a, String b, boolean useStrictComparation) { return (useStrictComparation ? compareStrictly(a, b) : compareLoosely(a, b)); } /** * Compare phone numbers a and b, return true if they're identical * enough for caller ID purposes. * * - Compares from right to left * - requires MIN_MATCH (7) characters to match * - handles common trunk prefixes and international prefixes * (basically, everything except the Russian trunk prefix) * * Note that this method does not return false even when the two phone numbers * are not exactly same; rather; we can call this method "similar()", not "equals()". * * @hide */ public static boolean compareLoosely(String a, String b) { int ia, ib; int matched; int numNonDialableCharsInA = 0; int numNonDialableCharsInB = 0; if (a == null || b == null) return a == b; if (a.length() == 0 || b.length() == 0) { return false; } ia = indexOfLastNetworkChar (a); ib = indexOfLastNetworkChar (b); matched = 0; while (ia >= 0 && ib >=0) { char ca, cb; boolean skipCmp = false; ca = a.charAt(ia); if (!isDialable(ca)) { ia--; skipCmp = true; numNonDialableCharsInA++; } cb = b.charAt(ib); if (!isDialable(cb)) { ib--; skipCmp = true; numNonDialableCharsInB++; } if (!skipCmp) { if (cb != ca && ca != WILD && cb != WILD) { break; } ia--; ib--; matched++; } } if (matched < MIN_MATCH) { int effectiveALen = a.length() - numNonDialableCharsInA; int effectiveBLen = b.length() - numNonDialableCharsInB; // if the number of dialable chars in a and b match, but the matched chars < MIN_MATCH, // treat them as equal (i.e. 404-04 and 40404) if (effectiveALen == effectiveBLen && effectiveALen == matched) { return true; } return false; } // At least one string has matched completely; if (matched >= MIN_MATCH && (ia < 0 || ib < 0)) { return true; } /* * Now, what remains must be one of the following for a * match: * * - a '+' on one and a '00' or a '011' on the other * - a '0' on one and a (+,00) on the other * (for this, a '0' and a '00' prefix would have succeeded above) */ if (matchIntlPrefix(a, ia + 1) && matchIntlPrefix (b, ib +1) ) { return true; } if (matchTrunkPrefix(a, ia + 1) && matchIntlPrefixAndCC(b, ib +1) ) { return true; } if (matchTrunkPrefix(b, ib + 1) && matchIntlPrefixAndCC(a, ia +1) ) { return true; } return false; } /** * @hide */ public static boolean compareStrictly(String a, String b) { return compareStrictly(a, b, true); } /** * @hide */ public static boolean compareStrictly(String a, String b, boolean acceptInvalidCCCPrefix) { if (a == null || b == null) { return a == b; } else if (a.length() == 0 && b.length() == 0) { return false; } int forwardIndexA = 0; int forwardIndexB = 0; CountryCallingCodeAndNewIndex cccA = tryGetCountryCallingCodeAndNewIndex(a, acceptInvalidCCCPrefix); CountryCallingCodeAndNewIndex cccB = tryGetCountryCallingCodeAndNewIndex(b, acceptInvalidCCCPrefix); boolean bothHasCountryCallingCode = false; boolean okToIgnorePrefix = true; boolean trunkPrefixIsOmittedA = false; boolean trunkPrefixIsOmittedB = false; if (cccA != null && cccB != null) { if (cccA.countryCallingCode != cccB.countryCallingCode) { // Different Country Calling Code. Must be different phone number. return false; } // When both have ccc, do not ignore trunk prefix. Without this, // "+81123123" becomes same as "+810123123" (+81 == Japan) okToIgnorePrefix = false; bothHasCountryCallingCode = true; forwardIndexA = cccA.newIndex; forwardIndexB = cccB.newIndex; } else if (cccA == null && cccB == null) { // When both do not have ccc, do not ignore trunk prefix. Without this, // "123123" becomes same as "0123123" okToIgnorePrefix = false; } else { if (cccA != null) { forwardIndexA = cccA.newIndex; } else { int tmp = tryGetTrunkPrefixOmittedIndex(b, 0); if (tmp >= 0) { forwardIndexA = tmp; trunkPrefixIsOmittedA = true; } } if (cccB != null) { forwardIndexB = cccB.newIndex; } else { int tmp = tryGetTrunkPrefixOmittedIndex(b, 0); if (tmp >= 0) { forwardIndexB = tmp; trunkPrefixIsOmittedB = true; } } } int backwardIndexA = a.length() - 1; int backwardIndexB = b.length() - 1; while (backwardIndexA >= forwardIndexA && backwardIndexB >= forwardIndexB) { boolean skip_compare = false; final char chA = a.charAt(backwardIndexA); final char chB = b.charAt(backwardIndexB); if (isSeparator(chA)) { backwardIndexA--; skip_compare = true; } if (isSeparator(chB)) { backwardIndexB--; skip_compare = true; } if (!skip_compare) { if (chA != chB) { return false; } backwardIndexA--; backwardIndexB--; } } if (okToIgnorePrefix) { if ((trunkPrefixIsOmittedA && forwardIndexA <= backwardIndexA) || !checkPrefixIsIgnorable(a, forwardIndexA, backwardIndexA)) { if (acceptInvalidCCCPrefix) { // Maybe the code handling the special case for Thailand makes the // result garbled, so disable the code and try again. // e.g. "16610001234" must equal to "6610001234", but with // Thailand-case handling code, they become equal to each other. // // Note: we select simplicity rather than adding some complicated // logic here for performance(like "checking whether remaining // numbers are just 66 or not"), assuming inputs are small // enough. return compare(a, b, false); } else { return false; } } if ((trunkPrefixIsOmittedB && forwardIndexB <= backwardIndexB) || !checkPrefixIsIgnorable(b, forwardIndexA, backwardIndexB)) { if (acceptInvalidCCCPrefix) { return compare(a, b, false); } else { return false; } } } else { // In the US, 1-650-555-1234 must be equal to 650-555-1234, // while 090-1234-1234 must not be equal to 90-1234-1234 in Japan. // This request exists just in US (with 1 trunk (NDD) prefix). // In addition, "011 11 7005554141" must not equal to "+17005554141", // while "011 1 7005554141" must equal to "+17005554141" // // In this comparison, we ignore the prefix '1' just once, when // - at least either does not have CCC, or // - the remaining non-separator number is 1 boolean maybeNamp = !bothHasCountryCallingCode; while (backwardIndexA >= forwardIndexA) { final char chA = a.charAt(backwardIndexA); if (isDialable(chA)) { if (maybeNamp && tryGetISODigit(chA) == 1) { maybeNamp = false; } else { return false; } } backwardIndexA--; } while (backwardIndexB >= forwardIndexB) { final char chB = b.charAt(backwardIndexB); if (isDialable(chB)) { if (maybeNamp && tryGetISODigit(chB) == 1) { maybeNamp = false; } else { return false; } } backwardIndexB--; } } return true; } /** * Returns the rightmost MIN_MATCH (5) characters in the network portion * in *reversed* order * * This can be used to do a database lookup against the column * that stores getStrippedReversed() * * Returns null if phoneNumber == null */ public static String toCallerIDMinMatch(String phoneNumber) { String np = extractNetworkPortionAlt(phoneNumber); return internalGetStrippedReversed(np, MIN_MATCH); } /** * Returns the network portion reversed. * This string is intended to go into an index column for a * database lookup. * * Returns null if phoneNumber == null */ public static String getStrippedReversed(String phoneNumber) { String np = extractNetworkPortionAlt(phoneNumber); if (np == null) return null; return internalGetStrippedReversed(np, np.length()); } /** * Returns the last numDigits of the reversed phone number * Returns null if np == null */ private static String internalGetStrippedReversed(String np, int numDigits) { if (np == null) return null; StringBuilder ret = new StringBuilder(numDigits); int length = np.length(); for (int i = length - 1, s = length ; i >= 0 && (s - i) <= numDigits ; i-- ) { char c = np.charAt(i); ret.append(c); } return ret.toString(); } /** * Basically: makes sure there's a + in front of a * TOA_International number * * Returns null if s == null */ public static String stringFromStringAndTOA(String s, int TOA) { if (s == null) return null; if (TOA == TOA_International && s.length() > 0 && s.charAt(0) != '+') { return "+" + s; } return s; } /** * Returns the TOA for the given dial string * Basically, returns TOA_International if there's a + prefix */ public static int toaFromString(String s) { if (s != null && s.length() > 0 && s.charAt(0) == '+') { return TOA_International; } return TOA_Unknown; } /** * 3GPP TS 24.008 10.5.4.7 * Called Party BCD Number * * See Also TS 51.011 10.5.1 "dialing number/ssc string" * and TS 11.11 "10.3.1 EF adn (Abbreviated dialing numbers)" * * @param bytes the data buffer * @param offset should point to the TOA (aka. TON/NPI) octet after the length byte * @param length is the number of bytes including TOA byte * and must be at least 2 * * @return partial string on invalid decode * * FIXME(mkf) support alphanumeric address type * currently implemented in SMSMessage.getAddress() */ public static String calledPartyBCDToString (byte[] bytes, int offset, int length) { boolean prependPlus = false; StringBuilder ret = new StringBuilder(1 + length * 2); if (length < 2) { return ""; } //Only TON field should be taken in consideration if ((bytes[offset] & 0xf0) == (TOA_International & 0xf0)) { prependPlus = true; } internalCalledPartyBCDFragmentToString( ret, bytes, offset + 1, length - 1); if (prependPlus && ret.length() == 0) { // If the only thing there is a prepended plus, return "" return ""; } if (prependPlus) { // This is an "international number" and should have // a plus prepended to the dialing number. But there // can also be GSM MMI codes as defined in TS 22.030 6.5.2 // so we need to handle those also. // // http://web.telia.com/~u47904776/gsmkode.htm // has a nice list of some of these GSM codes. // // Examples are: // **21*+886988171479# // **21*8311234567# // *21# // #21# // *#21# // *31#+11234567890 // #31#+18311234567 // #31#8311234567 // 18311234567 // +18311234567# // +18311234567 // Odd ball cases that some phones handled // where there is no dialing number so they // append the "+" // *21#+ // **21#+ String retString = ret.toString(); Pattern p = Pattern.compile("(^[#*])(.*)([#*])(.*)(#)$"); Matcher m = p.matcher(retString); if (m.matches()) { if ("".equals(m.group(2))) { // Started with two [#*] ends with # // So no dialing number and we'll just // append a +, this handles **21#+ ret = new StringBuilder(); ret.append(m.group(1)); ret.append(m.group(3)); ret.append(m.group(4)); ret.append(m.group(5)); ret.append("+"); } else { // Starts with [#*] and ends with # // Assume group 4 is a dialing number // such as *21*+1234554# ret = new StringBuilder(); ret.append(m.group(1)); ret.append(m.group(2)); ret.append(m.group(3)); ret.append("+"); ret.append(m.group(4)); ret.append(m.group(5)); } } else { p = Pattern.compile("(^[#*])(.*)([#*])(.*)"); m = p.matcher(retString); if (m.matches()) { // Starts with [#*] and only one other [#*] // Assume the data after last [#*] is dialing // number (i.e. group 4) such as *31#+11234567890. // This also includes the odd ball *21#+ ret = new StringBuilder(); ret.append(m.group(1)); ret.append(m.group(2)); ret.append(m.group(3)); ret.append("+"); ret.append(m.group(4)); } else { // Does NOT start with [#*] just prepend '+' ret = new StringBuilder(); ret.append('+'); ret.append(retString); } } } return ret.toString(); } private static void internalCalledPartyBCDFragmentToString( StringBuilder sb, byte [] bytes, int offset, int length) { for (int i = offset ; i < length + offset ; i++) { byte b; char c; c = bcdToChar((byte)(bytes[i] & 0xf)); if (c == 0) { return; } sb.append(c); // FIXME(mkf) TS 23.040 9.1.2.3 says // "if a mobile receives 1111 in a position prior to // the last semi-octet then processing shall commence with // the next semi-octet and the intervening // semi-octet shall be ignored" // How does this jive with 24.008 10.5.4.7 b = (byte)((bytes[i] >> 4) & 0xf); if (b == 0xf && i + 1 == length + offset) { //ignore final 0xf break; } c = bcdToChar(b); if (c == 0) { return; } sb.append(c); } } /** * Like calledPartyBCDToString, but field does not start with a * TOA byte. For example: SIM ADN extension fields */ public static String calledPartyBCDFragmentToString(byte [] bytes, int offset, int length) { StringBuilder ret = new StringBuilder(length * 2); internalCalledPartyBCDFragmentToString(ret, bytes, offset, length); return ret.toString(); } /** returns 0 on invalid value */ private static char bcdToChar(byte b) { if (b < 0xa) { return (char)('0' + b); } else switch (b) { case 0xa: return '*'; case 0xb: return '#'; case 0xc: return PAUSE; case 0xd: return WILD; default: return 0; } } private static int charToBCD(char c) { if (c >= '0' && c <= '9') { return c - '0'; } else if (c == '*') { return 0xa; } else if (c == '#') { return 0xb; } else if (c == PAUSE) { return 0xc; } else if (c == WILD) { return 0xd; } else if (c == WAIT) { return 0xe; } else { throw new RuntimeException ("invalid char for BCD " + c); } } /** * Return true iff the network portion of address is, * as far as we can tell on the device, suitable for use as an SMS * destination address. */ public static boolean isWellFormedSmsAddress(String address) { String networkPortion = PhoneNumberUtils.extractNetworkPortion(address); return (!(networkPortion.equals("+") || TextUtils.isEmpty(networkPortion))) && isDialable(networkPortion); } public static boolean isGlobalPhoneNumber(String phoneNumber) { if (TextUtils.isEmpty(phoneNumber)) { return false; } Matcher match = GLOBAL_PHONE_NUMBER_PATTERN.matcher(phoneNumber); return match.matches(); } private static boolean isDialable(String address) { for (int i = 0, count = address.length(); i < count; i++) { if (!isDialable(address.charAt(i))) { return false; } } return true; } private static boolean isNonSeparator(String address) { for (int i = 0, count = address.length(); i < count; i++) { if (!isNonSeparator(address.charAt(i))) { return false; } } return true; } /** * Note: calls extractNetworkPortion(), so do not use for * SIM EF[ADN] style records * * Returns null if network portion is empty. */ public static byte[] networkPortionToCalledPartyBCD(String s) { String networkPortion = extractNetworkPortion(s); return numberToCalledPartyBCDHelper(networkPortion, false); } /** * Same as {@link #networkPortionToCalledPartyBCD}, but includes a * one-byte length prefix. */ public static byte[] networkPortionToCalledPartyBCDWithLength(String s) { String networkPortion = extractNetworkPortion(s); return numberToCalledPartyBCDHelper(networkPortion, true); } /** * Convert a dialing number to BCD byte array * * @param number dialing number string * if the dialing number starts with '+', set to international TOA * @return BCD byte array */ public static byte[] numberToCalledPartyBCD(String number) { return numberToCalledPartyBCDHelper(number, false); } /** * If includeLength is true, prepend a one-byte length value to * the return array. */ private static byte[] numberToCalledPartyBCDHelper(String number, boolean includeLength) { int numberLenReal = number.length(); int numberLenEffective = numberLenReal; boolean hasPlus = number.indexOf('+') != -1; if (hasPlus) numberLenEffective--; if (numberLenEffective == 0) return null; int resultLen = (numberLenEffective + 1) / 2; // Encoded numbers require only 4 bits each. int extraBytes = 1; // Prepended TOA byte. if (includeLength) extraBytes++; // Optional prepended length byte. resultLen += extraBytes; byte[] result = new byte[resultLen]; int digitCount = 0; for (int i = 0; i < numberLenReal; i++) { char c = number.charAt(i); if (c == '+') continue; int shift = ((digitCount & 0x01) == 1) ? 4 : 0; result[extraBytes + (digitCount >> 1)] |= (byte)((charToBCD(c) & 0x0F) << shift); digitCount++; } // 1-fill any trailing odd nibble/quartet. if ((digitCount & 0x01) == 1) result[extraBytes + (digitCount >> 1)] |= 0xF0; int offset = 0; if (includeLength) result[offset++] = (byte)(resultLen - 1); result[offset] = (byte)(hasPlus ? TOA_International : TOA_Unknown); return result; } //================ Number formatting ========================= /** The current locale is unknown, look for a country code or don't format */ public static final int FORMAT_UNKNOWN = 0; /** NANP formatting */ public static final int FORMAT_NANP = 1; /** Japanese formatting */ public static final int FORMAT_JAPAN = 2; /** List of country codes for countries that use the NANP */ private static final String[] NANP_COUNTRIES = new String[] { "US", // United States "CA", // Canada "AS", // American Samoa "AI", // Anguilla "AG", // Antigua and Barbuda "BS", // Bahamas "BB", // Barbados "BM", // Bermuda "VG", // British Virgin Islands "KY", // Cayman Islands "DM", // Dominica "DO", // Dominican Republic "GD", // Grenada "GU", // Guam "JM", // Jamaica "PR", // Puerto Rico "MS", // Montserrat "MP", // Northern Mariana Islands "KN", // Saint Kitts and Nevis "LC", // Saint Lucia "VC", // Saint Vincent and the Grenadines "TT", // Trinidad and Tobago "TC", // Turks and Caicos Islands "VI", // U.S. Virgin Islands }; private static final String KOREA_ISO_COUNTRY_CODE = "KR"; private static final String JAPAN_ISO_COUNTRY_CODE = "JP"; /** * Breaks the given number down and formats it according to the rules * for the country the number is from. * * @param source The phone number to format * @return A locally acceptable formatting of the input, or the raw input if * formatting rules aren't known for the number * * @deprecated Use link #formatNumber(String phoneNumber, String defaultCountryIso) instead */ @Deprecated public static String formatNumber(String source) { SpannableStringBuilder text = new SpannableStringBuilder(source); formatNumber(text, getFormatTypeForLocale(Locale.getDefault())); return text.toString(); } /** * Formats the given number with the given formatting type. Currently * {@link #FORMAT_NANP} and {@link #FORMAT_JAPAN} are supported as a formating type. * * @param source the phone number to format * @param defaultFormattingType The default formatting rules to apply if the number does * not begin with +[country_code] * @return The phone number formatted with the given formatting type. * * @hide * @deprecated Use link #formatNumber(String phoneNumber, String defaultCountryIso) instead */ @Deprecated public static String formatNumber(String source, int defaultFormattingType) { SpannableStringBuilder text = new SpannableStringBuilder(source); formatNumber(text, defaultFormattingType); return text.toString(); } /** * Returns the phone number formatting type for the given locale. * * @param locale The locale of interest, usually {@link Locale#getDefault()} * @return The formatting type for the given locale, or FORMAT_UNKNOWN if the formatting * rules are not known for the given locale * * @deprecated Use link #formatNumber(String phoneNumber, String defaultCountryIso) instead */ @Deprecated public static int getFormatTypeForLocale(Locale locale) { String country = locale.getCountry(); return getFormatTypeFromCountryCode(country); } /** * Formats a phone number in-place. Currently {@link #FORMAT_JAPAN} and {@link #FORMAT_NANP} * is supported as a second argument. * * @param text The number to be formatted, will be modified with the formatting * @param defaultFormattingType The default formatting rules to apply if the number does * not begin with +[country_code] * * @deprecated Use link #formatNumber(String phoneNumber, String defaultCountryIso) instead */ @Deprecated public static void formatNumber(Editable text, int defaultFormattingType) { int formatType = defaultFormattingType; if (text.length() > 2 && text.charAt(0) == '+') { if (text.charAt(1) == '1') { formatType = FORMAT_NANP; } else if (text.length() >= 3 && text.charAt(1) == '8' && text.charAt(2) == '1') { formatType = FORMAT_JAPAN; } else { formatType = FORMAT_UNKNOWN; } } switch (formatType) { case FORMAT_NANP: formatNanpNumber(text); return; case FORMAT_JAPAN: formatJapaneseNumber(text); return; case FORMAT_UNKNOWN: removeDashes(text); return; } } private static final int NANP_STATE_DIGIT = 1; private static final int NANP_STATE_PLUS = 2; private static final int NANP_STATE_ONE = 3; private static final int NANP_STATE_DASH = 4; /** * Formats a phone number in-place using the NANP formatting rules. Numbers will be formatted * as: * *

* xxxxx * xxx-xxxx * xxx-xxx-xxxx * 1-xxx-xxx-xxxx * +1-xxx-xxx-xxxx *

* * @param text the number to be formatted, will be modified with the formatting * * @deprecated Use link #formatNumber(String phoneNumber, String defaultCountryIso) instead */ @Deprecated public static void formatNanpNumber(Editable text) { int length = text.length(); if (length > "+1-nnn-nnn-nnnn".length()) { // The string is too long to be formatted return; } else if (length <= 5) { // The string is either a shortcode or too short to be formatted return; } CharSequence saved = text.subSequence(0, length); // Strip the dashes first, as we're going to add them back removeDashes(text); length = text.length(); // When scanning the number we record where dashes need to be added, // if they're non-0 at the end of the scan the dashes will be added in // the proper places. int dashPositions[] = new int[3]; int numDashes = 0; int state = NANP_STATE_DIGIT; int numDigits = 0; for (int i = 0; i < length; i++) { char c = text.charAt(i); switch (c) { case '1': if (numDigits == 0 || state == NANP_STATE_PLUS) { state = NANP_STATE_ONE; break; } // fall through case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '0': if (state == NANP_STATE_PLUS) { // Only NANP number supported for now text.replace(0, length, saved); return; } else if (state == NANP_STATE_ONE) { // Found either +1 or 1, follow it up with a dash dashPositions[numDashes++] = i; } else if (state != NANP_STATE_DASH && (numDigits == 3 || numDigits == 6)) { // Found a digit that should be after a dash that isn't dashPositions[numDashes++] = i; } state = NANP_STATE_DIGIT; numDigits++; break; case '-': state = NANP_STATE_DASH; break; case '+': if (i == 0) { // Plus is only allowed as the first character state = NANP_STATE_PLUS; break; } // Fall through default: // Unknown character, bail on formatting text.replace(0, length, saved); return; } } if (numDigits == 7) { // With 7 digits we want xxx-xxxx, not xxx-xxx-x numDashes--; } // Actually put the dashes in place for (int i = 0; i < numDashes; i++) { int pos = dashPositions[i]; text.replace(pos + i, pos + i, "-"); } // Remove trailing dashes int len = text.length(); while (len > 0) { if (text.charAt(len - 1) == '-') { text.delete(len - 1, len); len--; } else { break; } } } /** * Formats a phone number in-place using the Japanese formatting rules. * Numbers will be formatted as: * *

* 03-xxxx-xxxx * 090-xxxx-xxxx * 0120-xxx-xxx * +81-3-xxxx-xxxx * +81-90-xxxx-xxxx *

* * @param text the number to be formatted, will be modified with * the formatting * * @deprecated Use link #formatNumber(String phoneNumber, String defaultCountryIso) instead */ @Deprecated public static void formatJapaneseNumber(Editable text) { JapanesePhoneNumberFormatter.format(text); } /** * Removes all dashes from the number. * * @param text the number to clear from dashes */ private static void removeDashes(Editable text) { int p = 0; while (p < text.length()) { if (text.charAt(p) == '-') { text.delete(p, p + 1); } else { p++; } } } /** * Formats the specified {@code phoneNumber} to the E.164 representation. * * @param phoneNumber the phone number to format. * @param defaultCountryIso the ISO 3166-1 two letters country code. * @return the E.164 representation, or null if the given phone number is not valid. */ public static String formatNumberToE164(String phoneNumber, String defaultCountryIso) { return formatNumberInternal(phoneNumber, defaultCountryIso, PhoneNumberFormat.E164); } /** * Formats the specified {@code phoneNumber} to the RFC3966 representation. * * @param phoneNumber the phone number to format. * @param defaultCountryIso the ISO 3166-1 two letters country code. * @return the RFC3966 representation, or null if the given phone number is not valid. */ public static String formatNumberToRFC3966(String phoneNumber, String defaultCountryIso) { return formatNumberInternal(phoneNumber, defaultCountryIso, PhoneNumberFormat.RFC3966); } /** * Formats the raw phone number (string) using the specified {@code formatIdentifier}. *

* The given phone number must have an area code and could have a country code. *

* The defaultCountryIso is used to validate the given number and generate the formatted number * if the specified number doesn't have a country code. * * @param rawPhoneNumber The phone number to format. * @param defaultCountryIso The ISO 3166-1 two letters country code. * @param formatIdentifier The (enum) identifier of the desired format. * @return the formatted representation, or null if the specified number is not valid. */ private static String formatNumberInternal( String rawPhoneNumber, String defaultCountryIso, PhoneNumberFormat formatIdentifier) { PhoneNumberUtil util = PhoneNumberUtil.getInstance(); try { PhoneNumber phoneNumber = util.parse(rawPhoneNumber, defaultCountryIso); if (util.isValidNumber(phoneNumber)) { return util.format(phoneNumber, formatIdentifier); } } catch (NumberParseException ignored) { } return null; } /** * Determines if a {@param phoneNumber} is international if dialed from * {@param defaultCountryIso}. * * @param phoneNumber The phone number. * @param defaultCountryIso The current country ISO. * @return {@code true} if the number is international, {@code false} otherwise. * @hide */ public static boolean isInternationalNumber(String phoneNumber, String defaultCountryIso) { // If no phone number is provided, it can't be international. if (TextUtils.isEmpty(phoneNumber)) { return false; } // If it starts with # or * its not international. if (phoneNumber.startsWith("#") || phoneNumber.startsWith("*")) { return false; } PhoneNumberUtil util = PhoneNumberUtil.getInstance(); try { PhoneNumber pn = util.parseAndKeepRawInput(phoneNumber, defaultCountryIso); return pn.getCountryCode() != util.getCountryCodeForRegion(defaultCountryIso); } catch (NumberParseException e) { return false; } } /** * Format a phone number. *

* If the given number doesn't have the country code, the phone will be * formatted to the default country's convention. * * @param phoneNumber * the number to be formatted. * @param defaultCountryIso * the ISO 3166-1 two letters country code whose convention will * be used if the given number doesn't have the country code. * @return the formatted number, or null if the given number is not valid. */ public static String formatNumber(String phoneNumber, String defaultCountryIso) { // Do not attempt to format numbers that start with a hash or star symbol. if (phoneNumber.startsWith("#") || phoneNumber.startsWith("*")) { return phoneNumber; } PhoneNumberUtil util = PhoneNumberUtil.getInstance(); String result = null; try { PhoneNumber pn = util.parseAndKeepRawInput(phoneNumber, defaultCountryIso); if (KOREA_ISO_COUNTRY_CODE.equalsIgnoreCase(defaultCountryIso) && (pn.getCountryCode() == util.getCountryCodeForRegion(KOREA_ISO_COUNTRY_CODE)) && (pn.getCountryCodeSource() == PhoneNumber.CountryCodeSource.FROM_NUMBER_WITH_PLUS_SIGN)) { /** * Need to reformat any local Korean phone numbers (when the user is in Korea) with * country code to corresponding national format which would replace the leading * +82 with 0. */ result = util.format(pn, PhoneNumberUtil.PhoneNumberFormat.NATIONAL); } else if (JAPAN_ISO_COUNTRY_CODE.equalsIgnoreCase(defaultCountryIso) && pn.getCountryCode() == util.getCountryCodeForRegion(JAPAN_ISO_COUNTRY_CODE) && (pn.getCountryCodeSource() == PhoneNumber.CountryCodeSource.FROM_NUMBER_WITH_PLUS_SIGN)) { /** * Need to reformat Japanese phone numbers (when user is in Japan) with the national * dialing format. */ result = util.format(pn, PhoneNumberUtil.PhoneNumberFormat.NATIONAL); } else { result = util.formatInOriginalFormat(pn, defaultCountryIso); } } catch (NumberParseException e) { } return result; } /** * Format the phone number only if the given number hasn't been formatted. *

* The number which has only dailable character is treated as not being * formatted. * * @param phoneNumber * the number to be formatted. * @param phoneNumberE164 * the E164 format number whose country code is used if the given * phoneNumber doesn't have the country code. * @param defaultCountryIso * the ISO 3166-1 two letters country code whose convention will * be used if the phoneNumberE164 is null or invalid, or if phoneNumber * contains IDD. * @return the formatted number if the given number has been formatted, * otherwise, return the given number. */ public static String formatNumber( String phoneNumber, String phoneNumberE164, String defaultCountryIso) { int len = phoneNumber.length(); for (int i = 0; i < len; i++) { if (!isDialable(phoneNumber.charAt(i))) { return phoneNumber; } } PhoneNumberUtil util = PhoneNumberUtil.getInstance(); // Get the country code from phoneNumberE164 if (phoneNumberE164 != null && phoneNumberE164.length() >= 2 && phoneNumberE164.charAt(0) == '+') { try { // The number to be parsed is in E164 format, so the default region used doesn't // matter. PhoneNumber pn = util.parse(phoneNumberE164, "ZZ"); String regionCode = util.getRegionCodeForNumber(pn); if (!TextUtils.isEmpty(regionCode) && // This makes sure phoneNumber doesn't contain an IDD normalizeNumber(phoneNumber).indexOf(phoneNumberE164.substring(1)) <= 0) { defaultCountryIso = regionCode; } } catch (NumberParseException e) { } } String result = formatNumber(phoneNumber, defaultCountryIso); return result != null ? result : phoneNumber; } /** * Normalize a phone number by removing the characters other than digits. If * the given number has keypad letters, the letters will be converted to * digits first. * * @param phoneNumber the number to be normalized. * @return the normalized number. */ public static String normalizeNumber(String phoneNumber) { if (TextUtils.isEmpty(phoneNumber)) { return ""; } StringBuilder sb = new StringBuilder(); int len = phoneNumber.length(); for (int i = 0; i < len; i++) { char c = phoneNumber.charAt(i); // Character.digit() supports ASCII and Unicode digits (fullwidth, Arabic-Indic, etc.) int digit = Character.digit(c, 10); if (digit != -1) { sb.append(digit); } else if (sb.length() == 0 && c == '+') { sb.append(c); } else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { return normalizeNumber(PhoneNumberUtils.convertKeypadLettersToDigits(phoneNumber)); } } return sb.toString(); } /** * Replaces all unicode(e.g. Arabic, Persian) digits with their decimal digit equivalents. * * @param number the number to perform the replacement on. * @return the replaced number. */ public static String replaceUnicodeDigits(String number) { StringBuilder normalizedDigits = new StringBuilder(number.length()); for (char c : number.toCharArray()) { int digit = Character.digit(c, 10); if (digit != -1) { normalizedDigits.append(digit); } else { normalizedDigits.append(c); } } return normalizedDigits.toString(); } // Three and four digit phone numbers for either special services, // or 3-6 digit addresses from the network (eg carrier-originated SMS messages) should // not match. // // This constant used to be 5, but SMS short codes has increased in length and // can be easily 6 digits now days. Most countries have SMS short code length between // 3 to 6 digits. The exceptions are // // Australia: Short codes are six or eight digits in length, starting with the prefix "19" // followed by an additional four or six digits and two. // Czechia: Codes are seven digits in length for MO and five (not billed) or // eight (billed) for MT direction // // see http://en.wikipedia.org/wiki/Short_code#Regional_differences for reference // // However, in order to loose match 650-555-1212 and 555-1212, we need to set the min match // to 7. static final int MIN_MATCH = 7; /** * Checks a given number against the list of * emergency numbers provided by the RIL and SIM card. * * @param number the number to look up. * @return true if the number is in the list of emergency numbers * listed in the RIL / SIM, otherwise return false. */ public static boolean isEmergencyNumber(String number) { return isEmergencyNumber(getDefaultVoiceSubId(), number); } /** * Checks a given number against the list of * emergency numbers provided by the RIL and SIM card. * * @param subId the subscription id of the SIM. * @param number the number to look up. * @return true if the number is in the list of emergency numbers * listed in the RIL / SIM, otherwise return false. * @hide */ public static boolean isEmergencyNumber(int subId, String number) { // Return true only if the specified number *exactly* matches // one of the emergency numbers listed by the RIL / SIM. return isEmergencyNumberInternal(subId, number, true /* useExactMatch */); } /** * Checks if given number might *potentially* result in * a call to an emergency service on the current network. * * Specifically, this method will return true if the specified number * is an emergency number according to the list managed by the RIL or * SIM, *or* if the specified number simply starts with the same * digits as any of the emergency numbers listed in the RIL / SIM. * * This method is intended for internal use by the phone app when * deciding whether to allow ACTION_CALL intents from 3rd party apps * (where we're required to *not* allow emergency calls to be placed.) * * @param number the number to look up. * @return true if the number is in the list of emergency numbers * listed in the RIL / SIM, *or* if the number starts with the * same digits as any of those emergency numbers. * * @hide */ public static boolean isPotentialEmergencyNumber(String number) { return isPotentialEmergencyNumber(getDefaultVoiceSubId(), number); } /** * Checks if given number might *potentially* result in * a call to an emergency service on the current network. * * Specifically, this method will return true if the specified number * is an emergency number according to the list managed by the RIL or * SIM, *or* if the specified number simply starts with the same * digits as any of the emergency numbers listed in the RIL / SIM. * * This method is intended for internal use by the phone app when * deciding whether to allow ACTION_CALL intents from 3rd party apps * (where we're required to *not* allow emergency calls to be placed.) * * @param subId the subscription id of the SIM. * @param number the number to look up. * @return true if the number is in the list of emergency numbers * listed in the RIL / SIM, *or* if the number starts with the * same digits as any of those emergency numbers. * @hide */ public static boolean isPotentialEmergencyNumber(int subId, String number) { // Check against the emergency numbers listed by the RIL / SIM, // and *don't* require an exact match. return isEmergencyNumberInternal(subId, number, false /* useExactMatch */); } /** * Helper function for isEmergencyNumber(String) and * isPotentialEmergencyNumber(String). * * @param number the number to look up. * * @param useExactMatch if true, consider a number to be an emergency * number only if it *exactly* matches a number listed in * the RIL / SIM. If false, a number is considered to be an * emergency number if it simply starts with the same digits * as any of the emergency numbers listed in the RIL / SIM. * (Setting useExactMatch to false allows you to identify * number that could *potentially* result in emergency calls * since many networks will actually ignore trailing digits * after a valid emergency number.) * * @return true if the number is in the list of emergency numbers * listed in the RIL / sim, otherwise return false. */ private static boolean isEmergencyNumberInternal(String number, boolean useExactMatch) { return isEmergencyNumberInternal(getDefaultVoiceSubId(), number, useExactMatch); } /** * Helper function for isEmergencyNumber(String) and * isPotentialEmergencyNumber(String). * * @param subId the subscription id of the SIM. * @param number the number to look up. * * @param useExactMatch if true, consider a number to be an emergency * number only if it *exactly* matches a number listed in * the RIL / SIM. If false, a number is considered to be an * emergency number if it simply starts with the same digits * as any of the emergency numbers listed in the RIL / SIM. * (Setting useExactMatch to false allows you to identify * number that could *potentially* result in emergency calls * since many networks will actually ignore trailing digits * after a valid emergency number.) * * @return true if the number is in the list of emergency numbers * listed in the RIL / sim, otherwise return false. */ private static boolean isEmergencyNumberInternal(int subId, String number, boolean useExactMatch) { return isEmergencyNumberInternal(subId, number, null, useExactMatch); } /** * Checks if a given number is an emergency number for a specific country. * * @param number the number to look up. * @param defaultCountryIso the specific country which the number should be checked against * @return if the number is an emergency number for the specific country, then return true, * otherwise false * * @hide */ public static boolean isEmergencyNumber(String number, String defaultCountryIso) { return isEmergencyNumber(getDefaultVoiceSubId(), number, defaultCountryIso); } /** * Checks if a given number is an emergency number for a specific country. * * @param subId the subscription id of the SIM. * @param number the number to look up. * @param defaultCountryIso the specific country which the number should be checked against * @return if the number is an emergency number for the specific country, then return true, * otherwise false * @hide */ public static boolean isEmergencyNumber(int subId, String number, String defaultCountryIso) { return isEmergencyNumberInternal(subId, number, defaultCountryIso, true /* useExactMatch */); } /** * Checks if a given number might *potentially* result in a call to an * emergency service, for a specific country. * * Specifically, this method will return true if the specified number * is an emergency number in the specified country, *or* if the number * simply starts with the same digits as any emergency number for that * country. * * This method is intended for internal use by the phone app when * deciding whether to allow ACTION_CALL intents from 3rd party apps * (where we're required to *not* allow emergency calls to be placed.) * * @param number the number to look up. * @param defaultCountryIso the specific country which the number should be checked against * @return true if the number is an emergency number for the specific * country, *or* if the number starts with the same digits as * any of those emergency numbers. * * @hide */ public static boolean isPotentialEmergencyNumber(String number, String defaultCountryIso) { return isPotentialEmergencyNumber(getDefaultVoiceSubId(), number, defaultCountryIso); } /** * Checks if a given number might *potentially* result in a call to an * emergency service, for a specific country. * * Specifically, this method will return true if the specified number * is an emergency number in the specified country, *or* if the number * simply starts with the same digits as any emergency number for that * country. * * This method is intended for internal use by the phone app when * deciding whether to allow ACTION_CALL intents from 3rd party apps * (where we're required to *not* allow emergency calls to be placed.) * * @param subId the subscription id of the SIM. * @param number the number to look up. * @param defaultCountryIso the specific country which the number should be checked against * @return true if the number is an emergency number for the specific * country, *or* if the number starts with the same digits as * any of those emergency numbers. * @hide */ public static boolean isPotentialEmergencyNumber(int subId, String number, String defaultCountryIso) { return isEmergencyNumberInternal(subId, number, defaultCountryIso, false /* useExactMatch */); } /** * Helper function for isEmergencyNumber(String, String) and * isPotentialEmergencyNumber(String, String). * * @param number the number to look up. * @param defaultCountryIso the specific country which the number should be checked against * @param useExactMatch if true, consider a number to be an emergency * number only if it *exactly* matches a number listed in * the RIL / SIM. If false, a number is considered to be an * emergency number if it simply starts with the same digits * as any of the emergency numbers listed in the RIL / SIM. * * @return true if the number is an emergency number for the specified country. */ private static boolean isEmergencyNumberInternal(String number, String defaultCountryIso, boolean useExactMatch) { return isEmergencyNumberInternal(getDefaultVoiceSubId(), number, defaultCountryIso, useExactMatch); } /** * Helper function for isEmergencyNumber(String, String) and * isPotentialEmergencyNumber(String, String). * * @param subId the subscription id of the SIM. * @param number the number to look up. * @param defaultCountryIso the specific country which the number should be checked against * @param useExactMatch if true, consider a number to be an emergency * number only if it *exactly* matches a number listed in * the RIL / SIM. If false, a number is considered to be an * emergency number if it simply starts with the same digits * as any of the emergency numbers listed in the RIL / SIM. * * @return true if the number is an emergency number for the specified country. * @hide */ private static boolean isEmergencyNumberInternal(int subId, String number, String defaultCountryIso, boolean useExactMatch) { // If the number passed in is null, just return false: if (number == null) return false; // If the number passed in is a SIP address, return false, since the // concept of "emergency numbers" is only meaningful for calls placed // over the cell network. // (Be sure to do this check *before* calling extractNetworkPortionAlt(), // since the whole point of extractNetworkPortionAlt() is to filter out // any non-dialable characters (which would turn 'abc911def@example.com' // into '911', for example.)) if (isUriNumber(number)) { return false; } // Strip the separators from the number before comparing it // to the list. number = extractNetworkPortionAlt(number); String emergencyNumbers = ""; int slotId = SubscriptionManager.getSlotIndex(subId); // retrieve the list of emergency numbers // check read-write ecclist property first String ecclist = (slotId <= 0) ? "ril.ecclist" : ("ril.ecclist" + slotId); emergencyNumbers = SystemProperties.get(ecclist, ""); Rlog.d(LOG_TAG, "slotId:" + slotId + " subId:" + subId + " country:" + defaultCountryIso + " emergencyNumbers: " + emergencyNumbers); if (TextUtils.isEmpty(emergencyNumbers)) { // then read-only ecclist property since old RIL only uses this emergencyNumbers = SystemProperties.get("ro.ril.ecclist"); } if (!TextUtils.isEmpty(emergencyNumbers)) { // searches through the comma-separated list for a match, // return true if one is found. for (String emergencyNum : emergencyNumbers.split(",")) { // It is not possible to append additional digits to an emergency number to dial // the number in Brazil - it won't connect. if (useExactMatch || "BR".equalsIgnoreCase(defaultCountryIso)) { if (number.equals(emergencyNum)) { return true; } } else { if (number.startsWith(emergencyNum)) { return true; } } } // no matches found against the list! return false; } Rlog.d(LOG_TAG, "System property doesn't provide any emergency numbers." + " Use embedded logic for determining ones."); // If slot id is invalid, means that there is no sim card. // According spec 3GPP TS22.101, the following numbers should be // ECC numbers when SIM/USIM is not present. emergencyNumbers = ((slotId < 0) ? "112,911,000,08,110,118,119,999" : "112,911"); for (String emergencyNum : emergencyNumbers.split(",")) { if (useExactMatch) { if (number.equals(emergencyNum)) { return true; } } else { if (number.startsWith(emergencyNum)) { return true; } } } // No ecclist system property, so use our own list. if (defaultCountryIso != null) { ShortNumberInfo info = ShortNumberInfo.getInstance(); if (useExactMatch) { return info.isEmergencyNumber(number, defaultCountryIso); } else { return info.connectsToEmergencyNumber(number, defaultCountryIso); } } return false; } /** * Checks if a given number is an emergency number for the country that the user is in. * * @param number the number to look up. * @param context the specific context which the number should be checked against * @return true if the specified number is an emergency number for the country the user * is currently in. */ public static boolean isLocalEmergencyNumber(Context context, String number) { return isLocalEmergencyNumber(context, getDefaultVoiceSubId(), number); } /** * Checks if a given number is an emergency number for the country that the user is in. * * @param subId the subscription id of the SIM. * @param number the number to look up. * @param context the specific context which the number should be checked against * @return true if the specified number is an emergency number for the country the user * is currently in. * @hide */ public static boolean isLocalEmergencyNumber(Context context, int subId, String number) { return isLocalEmergencyNumberInternal(subId, number, context, true /* useExactMatch */); } /** * Checks if a given number might *potentially* result in a call to an * emergency service, for the country that the user is in. The current * country is determined using the CountryDetector. * * Specifically, this method will return true if the specified number * is an emergency number in the current country, *or* if the number * simply starts with the same digits as any emergency number for the * current country. * * This method is intended for internal use by the phone app when * deciding whether to allow ACTION_CALL intents from 3rd party apps * (where we're required to *not* allow emergency calls to be placed.) * * @param number the number to look up. * @param context the specific context which the number should be checked against * @return true if the specified number is an emergency number for a local country, based on the * CountryDetector. * * @see android.location.CountryDetector * @hide */ public static boolean isPotentialLocalEmergencyNumber(Context context, String number) { return isPotentialLocalEmergencyNumber(context, getDefaultVoiceSubId(), number); } /** * Checks if a given number might *potentially* result in a call to an * emergency service, for the country that the user is in. The current * country is determined using the CountryDetector. * * Specifically, this method will return true if the specified number * is an emergency number in the current country, *or* if the number * simply starts with the same digits as any emergency number for the * current country. * * This method is intended for internal use by the phone app when * deciding whether to allow ACTION_CALL intents from 3rd party apps * (where we're required to *not* allow emergency calls to be placed.) * * @param subId the subscription id of the SIM. * @param number the number to look up. * @param context the specific context which the number should be checked against * @return true if the specified number is an emergency number for a local country, based on the * CountryDetector. * * @hide */ public static boolean isPotentialLocalEmergencyNumber(Context context, int subId, String number) { return isLocalEmergencyNumberInternal(subId, number, context, false /* useExactMatch */); } /** * Helper function for isLocalEmergencyNumber() and * isPotentialLocalEmergencyNumber(). * * @param number the number to look up. * @param context the specific context which the number should be checked against * @param useExactMatch if true, consider a number to be an emergency * number only if it *exactly* matches a number listed in * the RIL / SIM. If false, a number is considered to be an * emergency number if it simply starts with the same digits * as any of the emergency numbers listed in the RIL / SIM. * * @return true if the specified number is an emergency number for a * local country, based on the CountryDetector. * * @see android.location.CountryDetector * @hide */ private static boolean isLocalEmergencyNumberInternal(String number, Context context, boolean useExactMatch) { return isLocalEmergencyNumberInternal(getDefaultVoiceSubId(), number, context, useExactMatch); } /** * Helper function for isLocalEmergencyNumber() and * isPotentialLocalEmergencyNumber(). * * @param subId the subscription id of the SIM. * @param number the number to look up. * @param context the specific context which the number should be checked against * @param useExactMatch if true, consider a number to be an emergency * number only if it *exactly* matches a number listed in * the RIL / SIM. If false, a number is considered to be an * emergency number if it simply starts with the same digits * as any of the emergency numbers listed in the RIL / SIM. * * @return true if the specified number is an emergency number for a * local country, based on the CountryDetector. * @hide */ private static boolean isLocalEmergencyNumberInternal(int subId, String number, Context context, boolean useExactMatch) { String countryIso; CountryDetector detector = (CountryDetector) context.getSystemService( Context.COUNTRY_DETECTOR); if (detector != null && detector.detectCountry() != null) { countryIso = detector.detectCountry().getCountryIso(); } else { Locale locale = context.getResources().getConfiguration().locale; countryIso = locale.getCountry(); Rlog.w(LOG_TAG, "No CountryDetector; falling back to countryIso based on locale: " + countryIso); } return isEmergencyNumberInternal(subId, number, countryIso, useExactMatch); } /** * isVoiceMailNumber: checks a given number against the voicemail * number provided by the RIL and SIM card. The caller must have * the READ_PHONE_STATE credential. * * @param number the number to look up. * @return true if the number is in the list of voicemail. False * otherwise, including if the caller does not have the permission * to read the VM number. */ public static boolean isVoiceMailNumber(String number) { return isVoiceMailNumber(SubscriptionManager.getDefaultSubscriptionId(), number); } /** * isVoiceMailNumber: checks a given number against the voicemail * number provided by the RIL and SIM card. The caller must have * the READ_PHONE_STATE credential. * * @param subId the subscription id of the SIM. * @param number the number to look up. * @return true if the number is in the list of voicemail. False * otherwise, including if the caller does not have the permission * to read the VM number. * @hide */ public static boolean isVoiceMailNumber(int subId, String number) { return isVoiceMailNumber(null, subId, number); } /** * isVoiceMailNumber: checks a given number against the voicemail * number provided by the RIL and SIM card. The caller must have * the READ_PHONE_STATE credential. * * @param context {@link Context}. * @param subId the subscription id of the SIM. * @param number the number to look up. * @return true if the number is in the list of voicemail. False * otherwise, including if the caller does not have the permission * to read the VM number. * @hide */ public static boolean isVoiceMailNumber(Context context, int subId, String number) { String vmNumber, mdn; try { final TelephonyManager tm; if (context == null) { tm = TelephonyManager.getDefault(); if (DBG) log("isVoiceMailNumber: default tm"); } else { tm = TelephonyManager.from(context); if (DBG) log("isVoiceMailNumber: tm from context"); } vmNumber = tm.getVoiceMailNumber(subId); mdn = tm.getLine1Number(subId); if (DBG) log("isVoiceMailNumber: mdn=" + mdn + ", vmNumber=" + vmNumber + ", number=" + number); } catch (SecurityException ex) { if (DBG) log("isVoiceMailNumber: SecurityExcpetion caught"); return false; } // Strip the separators from the number before comparing it // to the list. number = extractNetworkPortionAlt(number); if (TextUtils.isEmpty(number)) { if (DBG) log("isVoiceMailNumber: number is empty after stripping"); return false; } // check if the carrier considers MDN to be an additional voicemail number boolean compareWithMdn = false; if (context != null) { CarrierConfigManager configManager = (CarrierConfigManager) context.getSystemService(Context.CARRIER_CONFIG_SERVICE); if (configManager != null) { PersistableBundle b = configManager.getConfigForSubId(subId); if (b != null) { compareWithMdn = b.getBoolean(CarrierConfigManager. KEY_MDN_IS_ADDITIONAL_VOICEMAIL_NUMBER_BOOL); if (DBG) log("isVoiceMailNumber: compareWithMdn=" + compareWithMdn); } } } if (compareWithMdn) { if (DBG) log("isVoiceMailNumber: treating mdn as additional vm number"); return compare(number, vmNumber) || compare(number, mdn); } else { if (DBG) log("isVoiceMailNumber: returning regular compare"); return compare(number, vmNumber); } } /** * Translates any alphabetic letters (i.e. [A-Za-z]) in the * specified phone number into the equivalent numeric digits, * according to the phone keypad letter mapping described in * ITU E.161 and ISO/IEC 9995-8. * * @return the input string, with alpha letters converted to numeric * digits using the phone keypad letter mapping. For example, * an input of "1-800-GOOG-411" will return "1-800-4664-411". */ public static String convertKeypadLettersToDigits(String input) { if (input == null) { return input; } int len = input.length(); if (len == 0) { return input; } char[] out = input.toCharArray(); for (int i = 0; i < len; i++) { char c = out[i]; // If this char isn't in KEYPAD_MAP at all, just leave it alone. out[i] = (char) KEYPAD_MAP.get(c, c); } return new String(out); } /** * The phone keypad letter mapping (see ITU E.161 or ISO/IEC 9995-8.) * TODO: This should come from a resource. */ private static final SparseIntArray KEYPAD_MAP = new SparseIntArray(); static { KEYPAD_MAP.put('a', '2'); KEYPAD_MAP.put('b', '2'); KEYPAD_MAP.put('c', '2'); KEYPAD_MAP.put('A', '2'); KEYPAD_MAP.put('B', '2'); KEYPAD_MAP.put('C', '2'); KEYPAD_MAP.put('d', '3'); KEYPAD_MAP.put('e', '3'); KEYPAD_MAP.put('f', '3'); KEYPAD_MAP.put('D', '3'); KEYPAD_MAP.put('E', '3'); KEYPAD_MAP.put('F', '3'); KEYPAD_MAP.put('g', '4'); KEYPAD_MAP.put('h', '4'); KEYPAD_MAP.put('i', '4'); KEYPAD_MAP.put('G', '4'); KEYPAD_MAP.put('H', '4'); KEYPAD_MAP.put('I', '4'); KEYPAD_MAP.put('j', '5'); KEYPAD_MAP.put('k', '5'); KEYPAD_MAP.put('l', '5'); KEYPAD_MAP.put('J', '5'); KEYPAD_MAP.put('K', '5'); KEYPAD_MAP.put('L', '5'); KEYPAD_MAP.put('m', '6'); KEYPAD_MAP.put('n', '6'); KEYPAD_MAP.put('o', '6'); KEYPAD_MAP.put('M', '6'); KEYPAD_MAP.put('N', '6'); KEYPAD_MAP.put('O', '6'); KEYPAD_MAP.put('p', '7'); KEYPAD_MAP.put('q', '7'); KEYPAD_MAP.put('r', '7'); KEYPAD_MAP.put('s', '7'); KEYPAD_MAP.put('P', '7'); KEYPAD_MAP.put('Q', '7'); KEYPAD_MAP.put('R', '7'); KEYPAD_MAP.put('S', '7'); KEYPAD_MAP.put('t', '8'); KEYPAD_MAP.put('u', '8'); KEYPAD_MAP.put('v', '8'); KEYPAD_MAP.put('T', '8'); KEYPAD_MAP.put('U', '8'); KEYPAD_MAP.put('V', '8'); KEYPAD_MAP.put('w', '9'); KEYPAD_MAP.put('x', '9'); KEYPAD_MAP.put('y', '9'); KEYPAD_MAP.put('z', '9'); KEYPAD_MAP.put('W', '9'); KEYPAD_MAP.put('X', '9'); KEYPAD_MAP.put('Y', '9'); KEYPAD_MAP.put('Z', '9'); } //================ Plus Code formatting ========================= private static final char PLUS_SIGN_CHAR = '+'; private static final String PLUS_SIGN_STRING = "+"; private static final String NANP_IDP_STRING = "011"; private static final int NANP_LENGTH = 10; /** * This function checks if there is a plus sign (+) in the passed-in dialing number. * If there is, it processes the plus sign based on the default telephone * numbering plan of the system when the phone is activated and the current * telephone numbering plan of the system that the phone is camped on. * Currently, we only support the case that the default and current telephone * numbering plans are North American Numbering Plan(NANP). * * The passed-in dialStr should only contain the valid format as described below, * 1) the 1st character in the dialStr should be one of the really dialable * characters listed below * ISO-LATIN characters 0-9, *, # , + * 2) the dialStr should already strip out the separator characters, * every character in the dialStr should be one of the non separator characters * listed below * ISO-LATIN characters 0-9, *, # , +, WILD, WAIT, PAUSE * * Otherwise, this function returns the dial string passed in * * @param dialStr the original dial string * @return the converted dial string if the current/default countries belong to NANP, * and if there is the "+" in the original dial string. Otherwise, the original dial * string returns. * * This API is for CDMA only * * @hide TODO: pending API Council approval */ public static String cdmaCheckAndProcessPlusCode(String dialStr) { if (!TextUtils.isEmpty(dialStr)) { if (isReallyDialable(dialStr.charAt(0)) && isNonSeparator(dialStr)) { String currIso = TelephonyManager.getDefault().getNetworkCountryIso(); String defaultIso = TelephonyManager.getDefault().getSimCountryIso(); if (!TextUtils.isEmpty(currIso) && !TextUtils.isEmpty(defaultIso)) { return cdmaCheckAndProcessPlusCodeByNumberFormat(dialStr, getFormatTypeFromCountryCode(currIso), getFormatTypeFromCountryCode(defaultIso)); } } } return dialStr; } /** * Process phone number for CDMA, converting plus code using the home network number format. * This is used for outgoing SMS messages. * * @param dialStr the original dial string * @return the converted dial string * @hide for internal use */ public static String cdmaCheckAndProcessPlusCodeForSms(String dialStr) { if (!TextUtils.isEmpty(dialStr)) { if (isReallyDialable(dialStr.charAt(0)) && isNonSeparator(dialStr)) { String defaultIso = TelephonyManager.getDefault().getSimCountryIso(); if (!TextUtils.isEmpty(defaultIso)) { int format = getFormatTypeFromCountryCode(defaultIso); return cdmaCheckAndProcessPlusCodeByNumberFormat(dialStr, format, format); } } } return dialStr; } /** * This function should be called from checkAndProcessPlusCode only * And it is used for test purpose also. * * It checks the dial string by looping through the network portion, * post dial portion 1, post dial porting 2, etc. If there is any * plus sign, then process the plus sign. * Currently, this function supports the plus sign conversion within NANP only. * Specifically, it handles the plus sign in the following ways: * 1)+1NANP,remove +, e.g. * +18475797000 is converted to 18475797000, * 2)+NANP or +non-NANP Numbers,replace + with the current NANP IDP, e.g, * +8475797000 is converted to 0118475797000, * +11875767800 is converted to 01111875767800 * 3)+1NANP in post dial string(s), e.g. * 8475797000;+18475231753 is converted to 8475797000;18475231753 * * * @param dialStr the original dial string * @param currFormat the numbering system of the current country that the phone is camped on * @param defaultFormat the numbering system of the country that the phone is activated on * @return the converted dial string if the current/default countries belong to NANP, * and if there is the "+" in the original dial string. Otherwise, the original dial * string returns. * * @hide */ public static String cdmaCheckAndProcessPlusCodeByNumberFormat(String dialStr,int currFormat,int defaultFormat) { String retStr = dialStr; boolean useNanp = (currFormat == defaultFormat) && (currFormat == FORMAT_NANP); // Checks if the plus sign character is in the passed-in dial string if (dialStr != null && dialStr.lastIndexOf(PLUS_SIGN_STRING) != -1) { // Handle case where default and current telephone numbering plans are NANP. String postDialStr = null; String tempDialStr = dialStr; // Sets the retStr to null since the conversion will be performed below. retStr = null; if (DBG) log("checkAndProcessPlusCode,dialStr=" + dialStr); // This routine is to process the plus sign in the dial string by loop through // the network portion, post dial portion 1, post dial portion 2... etc. if // applied do { String networkDialStr; // Format the string based on the rules for the country the number is from, // and the current country the phone is camped if (useNanp) { networkDialStr = extractNetworkPortion(tempDialStr); } else { networkDialStr = extractNetworkPortionAlt(tempDialStr); } networkDialStr = processPlusCode(networkDialStr, useNanp); // Concatenates the string that is converted from network portion if (!TextUtils.isEmpty(networkDialStr)) { if (retStr == null) { retStr = networkDialStr; } else { retStr = retStr.concat(networkDialStr); } } else { // This should never happen since we checked the if dialStr is null // and if it contains the plus sign in the beginning of this function. // The plus sign is part of the network portion. Rlog.e("checkAndProcessPlusCode: null newDialStr", networkDialStr); return dialStr; } postDialStr = extractPostDialPortion(tempDialStr); if (!TextUtils.isEmpty(postDialStr)) { int dialableIndex = findDialableIndexFromPostDialStr(postDialStr); // dialableIndex should always be greater than 0 if (dialableIndex >= 1) { retStr = appendPwCharBackToOrigDialStr(dialableIndex, retStr,postDialStr); // Skips the P/W character, extracts the dialable portion tempDialStr = postDialStr.substring(dialableIndex); } else { // Non-dialable character such as P/W should not be at the end of // the dial string after P/W processing in GsmCdmaConnection.java // Set the postDialStr to "" to break out of the loop if (dialableIndex < 0) { postDialStr = ""; } Rlog.e("wrong postDialStr=", postDialStr); } } if (DBG) log("checkAndProcessPlusCode,postDialStr=" + postDialStr); } while (!TextUtils.isEmpty(postDialStr) && !TextUtils.isEmpty(tempDialStr)); } return retStr; } /** * Wrap the supplied {@code CharSequence} with a {@code TtsSpan}, annotating it as * containing a phone number in its entirety. * * @param phoneNumber A {@code CharSequence} the entirety of which represents a phone number. * @return A {@code CharSequence} with appropriate annotations. */ public static CharSequence createTtsSpannable(CharSequence phoneNumber) { if (phoneNumber == null) { return null; } Spannable spannable = Spannable.Factory.getInstance().newSpannable(phoneNumber); PhoneNumberUtils.addTtsSpan(spannable, 0, spannable.length()); return spannable; } /** * Attach a {@link TtsSpan} to the supplied {@code Spannable} at the indicated location, * annotating that location as containing a phone number. * * @param s A {@code Spannable} to annotate. * @param start The starting character position of the phone number in {@code s}. * @param endExclusive The position after the ending character in the phone number {@code s}. */ public static void addTtsSpan(Spannable s, int start, int endExclusive) { s.setSpan(createTtsSpan(s.subSequence(start, endExclusive).toString()), start, endExclusive, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } /** * Wrap the supplied {@code CharSequence} with a {@code TtsSpan}, annotating it as * containing a phone number in its entirety. * * @param phoneNumber A {@code CharSequence} the entirety of which represents a phone number. * @return A {@code CharSequence} with appropriate annotations. * @deprecated Renamed {@link #createTtsSpannable}. * * @hide */ @Deprecated public static CharSequence ttsSpanAsPhoneNumber(CharSequence phoneNumber) { return createTtsSpannable(phoneNumber); } /** * Attach a {@link TtsSpan} to the supplied {@code Spannable} at the indicated location, * annotating that location as containing a phone number. * * @param s A {@code Spannable} to annotate. * @param start The starting character position of the phone number in {@code s}. * @param end The ending character position of the phone number in {@code s}. * * @deprecated Renamed {@link #addTtsSpan}. * * @hide */ @Deprecated public static void ttsSpanAsPhoneNumber(Spannable s, int start, int end) { addTtsSpan(s, start, end); } /** * Create a {@code TtsSpan} for the supplied {@code String}. * * @param phoneNumberString A {@code String} the entirety of which represents a phone number. * @return A {@code TtsSpan} for {@param phoneNumberString}. */ public static TtsSpan createTtsSpan(String phoneNumberString) { if (phoneNumberString == null) { return null; } // Parse the phone number final PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance(); PhoneNumber phoneNumber = null; try { // Don't supply a defaultRegion so this fails for non-international numbers because // we don't want to TalkBalk to read a country code (e.g. +1) if it is not already // present phoneNumber = phoneNumberUtil.parse(phoneNumberString, /* defaultRegion */ null); } catch (NumberParseException ignored) { } // Build a telephone tts span final TtsSpan.TelephoneBuilder builder = new TtsSpan.TelephoneBuilder(); if (phoneNumber == null) { // Strip separators otherwise TalkBack will be silent // (this behavior was observed with TalkBalk 4.0.2 from their alpha channel) builder.setNumberParts(splitAtNonNumerics(phoneNumberString)); } else { if (phoneNumber.hasCountryCode()) { builder.setCountryCode(Integer.toString(phoneNumber.getCountryCode())); } builder.setNumberParts(Long.toString(phoneNumber.getNationalNumber())); } return builder.build(); } // Split a phone number like "+20(123)-456#" using spaces, ignoring anything that is not // a digit, to produce a result like "20 123 456". private static String splitAtNonNumerics(CharSequence number) { StringBuilder sb = new StringBuilder(number.length()); for (int i = 0; i < number.length(); i++) { sb.append(PhoneNumberUtils.isISODigit(number.charAt(i)) ? number.charAt(i) : " "); } // It is very important to remove extra spaces. At time of writing, any leading or trailing // spaces, or any sequence of more than one space, will confuse TalkBack and cause the TTS // span to be non-functional! return sb.toString().replaceAll(" +", " ").trim(); } private static String getCurrentIdp(boolean useNanp) { String ps = null; if (useNanp) { ps = NANP_IDP_STRING; } else { // in case, there is no IDD is found, we shouldn't convert it. ps = SystemProperties.get(PROPERTY_OPERATOR_IDP_STRING, PLUS_SIGN_STRING); } return ps; } private static boolean isTwoToNine (char c) { if (c >= '2' && c <= '9') { return true; } else { return false; } } private static int getFormatTypeFromCountryCode (String country) { // Check for the NANP countries int length = NANP_COUNTRIES.length; for (int i = 0; i < length; i++) { if (NANP_COUNTRIES[i].compareToIgnoreCase(country) == 0) { return FORMAT_NANP; } } if ("jp".compareToIgnoreCase(country) == 0) { return FORMAT_JAPAN; } return FORMAT_UNKNOWN; } /** * This function checks if the passed in string conforms to the NANP format * i.e. NXX-NXX-XXXX, N is any digit 2-9 and X is any digit 0-9 * @hide */ public static boolean isNanp (String dialStr) { boolean retVal = false; if (dialStr != null) { if (dialStr.length() == NANP_LENGTH) { if (isTwoToNine(dialStr.charAt(0)) && isTwoToNine(dialStr.charAt(3))) { retVal = true; for (int i=1; i 0 && countryCallingCodeCandidate < CCC_LENGTH && COUNTRY_CALLING_CALL[countryCallingCodeCandidate]; } /** * Returns integer corresponding to the input if input "ch" is * ISO-LATIN characters 0-9. * Returns -1 otherwise */ private static int tryGetISODigit(char ch) { if ('0' <= ch && ch <= '9') { return ch - '0'; } else { return -1; } } private static class CountryCallingCodeAndNewIndex { public final int countryCallingCode; public final int newIndex; public CountryCallingCodeAndNewIndex(int countryCode, int newIndex) { this.countryCallingCode = countryCode; this.newIndex = newIndex; } } /* * Note that this function does not strictly care the country calling code with * 3 length (like Morocco: +212), assuming it is enough to use the first two * digit to compare two phone numbers. */ private static CountryCallingCodeAndNewIndex tryGetCountryCallingCodeAndNewIndex( String str, boolean acceptThailandCase) { // Rough regexp: // ^[^0-9*#+]*((\+|0(0|11)\d\d?|166) [^0-9*#+] $ // 0 1 2 3 45 6 7 89 // // In all the states, this function ignores separator characters. // "166" is the special case for the call from Thailand to the US. Uguu! int state = 0; int ccc = 0; final int length = str.length(); for (int i = 0 ; i < length ; i++ ) { char ch = str.charAt(i); switch (state) { case 0: if (ch == '+') state = 1; else if (ch == '0') state = 2; else if (ch == '1') { if (acceptThailandCase) { state = 8; } else { return null; } } else if (isDialable(ch)) { return null; } break; case 2: if (ch == '0') state = 3; else if (ch == '1') state = 4; else if (isDialable(ch)) { return null; } break; case 4: if (ch == '1') state = 5; else if (isDialable(ch)) { return null; } break; case 1: case 3: case 5: case 6: case 7: { int ret = tryGetISODigit(ch); if (ret > 0) { ccc = ccc * 10 + ret; if (ccc >= 100 || isCountryCallingCode(ccc)) { return new CountryCallingCodeAndNewIndex(ccc, i + 1); } if (state == 1 || state == 3 || state == 5) { state = 6; } else { state++; } } else if (isDialable(ch)) { return null; } } break; case 8: if (ch == '6') state = 9; else if (isDialable(ch)) { return null; } break; case 9: if (ch == '6') { return new CountryCallingCodeAndNewIndex(66, i + 1); } else { return null; } default: return null; } } return null; } /** * Currently this function simply ignore the first digit assuming it is * trunk prefix. Actually trunk prefix is different in each country. * * e.g. * "+79161234567" equals "89161234567" (Russian trunk digit is 8) * "+33123456789" equals "0123456789" (French trunk digit is 0) * */ private static int tryGetTrunkPrefixOmittedIndex(String str, int currentIndex) { int length = str.length(); for (int i = currentIndex ; i < length ; i++) { final char ch = str.charAt(i); if (tryGetISODigit(ch) >= 0) { return i + 1; } else if (isDialable(ch)) { return -1; } } return -1; } /** * Return true if the prefix of "str" is "ignorable". Here, "ignorable" means * that "str" has only one digit and separator characters. The one digit is * assumed to be trunk prefix. */ private static boolean checkPrefixIsIgnorable(final String str, int forwardIndex, int backwardIndex) { boolean trunk_prefix_was_read = false; while (backwardIndex >= forwardIndex) { if (tryGetISODigit(str.charAt(backwardIndex)) >= 0) { if (trunk_prefix_was_read) { // More than one digit appeared, meaning that "a" and "b" // is different. return false; } else { // Ignore just one digit, assuming it is trunk prefix. trunk_prefix_was_read = true; } } else if (isDialable(str.charAt(backwardIndex))) { // Trunk prefix is a digit, not "*", "#"... return false; } backwardIndex--; } return true; } /** * Returns Default voice subscription Id. */ private static int getDefaultVoiceSubId() { return SubscriptionManager.getDefaultVoiceSubscriptionId(); } //==== End of utility methods used only in compareStrictly() ===== /* * The config held calling number conversion map, expected to convert to emergency number. */ private static final String[] CONVERT_TO_EMERGENCY_MAP = Resources.getSystem().getStringArray( com.android.internal.R.array.config_convert_to_emergency_number_map); /** * Check whether conversion to emergency number is enabled * * @return {@code true} when conversion to emergency numbers is enabled, * {@code false} otherwise * * @hide */ public static boolean isConvertToEmergencyNumberEnabled() { return CONVERT_TO_EMERGENCY_MAP != null && CONVERT_TO_EMERGENCY_MAP.length > 0; } /** * Converts to emergency number based on the conversion map. * The conversion map is declared as config_convert_to_emergency_number_map. * * Make sure {@link #isConvertToEmergencyNumberEnabled} is true before calling * this function. * * @return The converted emergency number if the number matches conversion map, * otherwise original number. * * @hide */ public static String convertToEmergencyNumber(String number) { if (TextUtils.isEmpty(number)) { return number; } String normalizedNumber = normalizeNumber(number); // The number is already emergency number. Skip conversion. if (isEmergencyNumber(normalizedNumber)) { return number; } for (String convertMap : CONVERT_TO_EMERGENCY_MAP) { if (DBG) log("convertToEmergencyNumber: " + convertMap); String[] entry = null; String[] filterNumbers = null; String convertedNumber = null; if (!TextUtils.isEmpty(convertMap)) { entry = convertMap.split(":"); } if (entry != null && entry.length == 2) { convertedNumber = entry[1]; if (!TextUtils.isEmpty(entry[0])) { filterNumbers = entry[0].split(","); } } // Skip if the format of entry is invalid if (TextUtils.isEmpty(convertedNumber) || filterNumbers == null || filterNumbers.length == 0) { continue; } for (String filterNumber : filterNumbers) { if (DBG) log("convertToEmergencyNumber: filterNumber = " + filterNumber + ", convertedNumber = " + convertedNumber); if (!TextUtils.isEmpty(filterNumber) && filterNumber.equals(normalizedNumber)) { if (DBG) log("convertToEmergencyNumber: Matched. Successfully converted to: " + convertedNumber); return convertedNumber; } } } return number; } }