| /* |
| * 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 android.content.Context; |
| import android.content.Intent; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.os.SystemProperties; |
| import android.provider.Contacts; |
| import android.text.Editable; |
| import android.text.SpannableStringBuilder; |
| import android.text.TextUtils; |
| import android.util.SparseIntArray; |
| |
| 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'; |
| |
| /* |
| * 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; |
| |
| /* |
| * 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; |
| } |
| |
| /** 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 |
| * <code>null</code> if the number cannot be found. |
| */ |
| public static String getNumberFromIntent(Intent intent, Context context) { |
| String number = null; |
| |
| Uri uri = intent.getData(); |
| String scheme = uri.getScheme(); |
| |
| if (scheme.equals("tel")) { |
| return uri.getSchemeSpecificPart(); |
| } |
| |
| if (scheme.equals("voicemail")) { |
| return TelephonyManager.getDefault().getVoiceMailNumber(); |
| } |
| |
| if (context == null) { |
| return null; |
| } |
| |
| String type = intent.resolveType(context); |
| |
| Cursor c = context.getContentResolver().query( |
| uri, new String[]{ Contacts.People.Phones.NUMBER }, |
| null, null, null); |
| if (c != null) { |
| try { |
| if (c.moveToFirst()) { |
| number = c.getString( |
| c.getColumnIndex(Contacts.People.Phones.NUMBER)); |
| } |
| } finally { |
| 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. |
| * |
| * Allows + only in the first position in the result string. |
| * |
| * 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); |
| boolean firstCharAdded = false; |
| |
| for (int i = 0; i < len; i++) { |
| char c = phoneNumber.charAt(i); |
| if (isDialable(c) && (c != '+' || !firstCharAdded)) { |
| firstCharAdded = true; |
| 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); |
| if (isNonSeparator(c)) { |
| 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; |
| } |
| } |
| |
| /** 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. |
| * |
| * - Compares from right to left |
| * - requires MIN_MATCH (5) characters to match |
| * - handles common trunk prefixes and international prefixes |
| * (basically, everything except the Russian trunk prefix) |
| * |
| * Tolerates nulls |
| */ |
| public static boolean |
| compare(String a, String b) { |
| int ia, ib; |
| int matched; |
| |
| 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; |
| } |
| |
| cb = b.charAt(ib); |
| |
| if (!isDialable(cb)) { |
| ib--; |
| skipCmp = true; |
| } |
| |
| if (!skipCmp) { |
| if (cb != ca && ca != WILD && cb != WILD) { |
| break; |
| } |
| ia--; ib--; matched++; |
| } |
| } |
| |
| if (matched < MIN_MATCH) { |
| int aLen = a.length(); |
| |
| // if the input strings match, but their lengths < MIN_MATCH, |
| // treat them as equal. |
| if (aLen == b.length() && aLen == 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)<country code> 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; |
| } |
| |
| /** |
| * 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 = extractNetworkPortion(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 = extractNetworkPortion(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; |
| } |
| |
| /** |
| * Phone numbers are stored in "lookup" form in the database |
| * as reversed strings to allow for caller ID lookup |
| * |
| * This method takes a phone number and makes a valid SQL "LIKE" |
| * string that will match the lookup form |
| * |
| */ |
| /** all of a up to len must be an international prefix or |
| * separators/non-dialing digits |
| */ |
| private static boolean |
| matchIntlPrefix(String a, int len) { |
| /* '([^0-9*#+pwn]\+[^0-9*#+pwn] | [^0-9*#+pwn]0(0|11)[^0-9*#+pwn] )$' */ |
| /* 0 1 2 3 45 */ |
| |
| int state = 0; |
| for (int i = 0 ; i < len ; i++) { |
| char c = a.charAt(i); |
| |
| switch (state) { |
| case 0: |
| if (c == '+') state = 1; |
| else if (c == '0') state = 2; |
| else if (isNonSeparator(c)) return false; |
| break; |
| |
| case 2: |
| if (c == '0') state = 3; |
| else if (c == '1') state = 4; |
| else if (isNonSeparator(c)) return false; |
| break; |
| |
| case 4: |
| if (c == '1') state = 5; |
| else if (isNonSeparator(c)) return false; |
| break; |
| |
| default: |
| if (isNonSeparator(c)) return false; |
| break; |
| |
| } |
| } |
| |
| return state == 1 || state == 3 || state == 5; |
| } |
| |
| /** |
| * 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 ""; |
| } |
| |
| if ((bytes[offset] & 0xff) == TOA_International) { |
| 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 is a |
| // 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 commense 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 { |
| throw new RuntimeException ("invalid char for BCD " + c); |
| } |
| } |
| |
| /** |
| * Note: calls extractNetworkPortion(), so do not use for |
| * SIM EF[ADN] style records |
| * |
| * Exceptions thrown if extractNetworkPortion(s).length() == 0 |
| */ |
| public static byte[] |
| networkPortionToCalledPartyBCD(String s) { |
| return numberToCalledPartyBCD(extractNetworkPortion(s)); |
| } |
| |
| /** |
| * Return true iff the network portion of <code>address</code> 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; |
| } |
| |
| /** |
| * Same as {@link #networkPortionToCalledPartyBCD}, but includes a |
| * one-byte length prefix. |
| */ |
| public static byte[] |
| networkPortionToCalledPartyBCDWithLength(String s) { |
| return numberToCalledPartyBCDWithLength(extractNetworkPortion(s)); |
| } |
| |
| /** |
| * Convert a dialing number to BCD byte array |
| * |
| * @param number dialing number string |
| * if the dialing number starts with '+', set to internationl TOA |
| * @return BCD byte array |
| */ |
| public static byte[] |
| numberToCalledPartyBCD(String number) { |
| // The extra byte required for '+' is taken into consideration while calculating |
| // length of ret. |
| int size = (hasPlus(number) ? number.length() - 1 : number.length()); |
| byte[] ret = new byte[(size + 1) / 2 + 1]; |
| |
| return numberToCalledPartyBCDHelper(ret, 0, number); |
| } |
| |
| /** |
| * Same as {@link #numberToCalledPartyBCD}, but includes a |
| * one-byte length prefix. |
| */ |
| private static byte[] |
| numberToCalledPartyBCDWithLength(String number) { |
| // The extra byte required for '+' is taken into consideration while calculating |
| // length of ret. |
| int size = (hasPlus(number) ? number.length() - 1 : number.length()); |
| int length = (size + 1) / 2 + 1; |
| byte[] ret = new byte[length + 1]; |
| |
| ret[0] = (byte) (length & 0xff); |
| return numberToCalledPartyBCDHelper(ret, 1, number); |
| } |
| |
| private static boolean |
| hasPlus(String s) { |
| return s.indexOf('+') >= 0; |
| } |
| |
| private static byte[] |
| numberToCalledPartyBCDHelper(byte[] ret, int offset, String number) { |
| if (hasPlus(number)) { |
| number = number.replaceAll("\\+", ""); |
| ret[offset] = (byte) TOA_International; |
| } else { |
| ret[offset] = (byte) TOA_Unknown; |
| } |
| |
| int size = number.length(); |
| int curChar = 0; |
| int countFullBytes = ret.length - offset - 1 - ((size - curChar) & 1); |
| for (int i = 1; i < 1 + countFullBytes; i++) { |
| ret[offset + i] |
| = (byte) ((charToBCD(number.charAt(curChar++))) |
| | (charToBCD(number.charAt(curChar++))) << 4); |
| } |
| |
| // The left-over octet for odd-length phone numbers should be |
| // filled with 0xf. |
| if (countFullBytes + offset < ret.length - 1) { |
| ret[ret.length - 1] |
| = (byte) (charToBCD(number.charAt(curChar)) |
| | (0xf << 4)); |
| } |
| return ret; |
| } |
| |
| /** all of 'a' up to len must match non-US trunk prefix ('0') */ |
| private static boolean |
| matchTrunkPrefix(String a, int len) { |
| boolean found; |
| |
| found = false; |
| |
| for (int i = 0 ; i < len ; i++) { |
| char c = a.charAt(i); |
| |
| if (c == '0' && !found) { |
| found = true; |
| } else if (isNonSeparator(c)) { |
| return false; |
| } |
| } |
| |
| return found; |
| } |
| |
| /** all of 'a' up to len must be a (+|00|011)country code) |
| * We're fast and loose with the country code. Any \d{1,3} matches */ |
| private static boolean |
| matchIntlPrefixAndCC(String a, int len) { |
| /* [^0-9*#+pwn]*(\+|0(0|11)\d\d?\d? [^0-9*#+pwn] $ */ |
| /* 0 1 2 3 45 6 7 8 */ |
| |
| int state = 0; |
| for (int i = 0 ; i < len ; i++ ) { |
| char c = a.charAt(i); |
| |
| switch (state) { |
| case 0: |
| if (c == '+') state = 1; |
| else if (c == '0') state = 2; |
| else if (isNonSeparator(c)) return false; |
| break; |
| |
| case 2: |
| if (c == '0') state = 3; |
| else if (c == '1') state = 4; |
| else if (isNonSeparator(c)) return false; |
| break; |
| |
| case 4: |
| if (c == '1') state = 5; |
| else if (isNonSeparator(c)) return false; |
| break; |
| |
| case 1: |
| case 3: |
| case 5: |
| if (isISODigit(c)) state = 6; |
| else if (isNonSeparator(c)) return false; |
| break; |
| |
| case 6: |
| case 7: |
| if (isISODigit(c)) state++; |
| else if (isNonSeparator(c)) return false; |
| break; |
| |
| default: |
| if (isNonSeparator(c)) return false; |
| } |
| } |
| |
| return state == 6 || state == 7 || state == 8; |
| } |
| |
| //================ 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 |
| "NP", // 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 |
| }; |
| |
| /** |
| * 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 |
| */ |
| public static String formatNumber(String source) { |
| SpannableStringBuilder text = new SpannableStringBuilder(source); |
| formatNumber(text, getFormatTypeForLocale(Locale.getDefault())); |
| 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 |
| */ |
| public static int getFormatTypeForLocale(Locale locale) { |
| String country = locale.getCountry(); |
| |
| // Check for the NANP countries |
| int length = NANP_COUNTRIES.length; |
| for (int i = 0; i < length; i++) { |
| if (NANP_COUNTRIES[i].equals(country)) { |
| return FORMAT_NANP; |
| } |
| } |
| if (locale.equals(Locale.JAPAN)) { |
| return FORMAT_JAPAN; |
| } |
| return FORMAT_UNKNOWN; |
| } |
| |
| /** |
| * Formats a phone number in-place. Currently only supports NANP formatting. |
| * |
| * @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> |
| */ |
| 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 { |
| return; |
| } |
| } |
| |
| switch (formatType) { |
| case FORMAT_NANP: |
| formatNanpNumber(text); |
| return; |
| case FORMAT_JAPAN: |
| formatJapaneseNumber(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: |
| * |
| * <p><code> |
| * xxx-xxxx |
| * xxx-xxx-xxxx |
| * 1-xxx-xxx-xxxx |
| * +1-xxx-xxx-xxxx |
| * </code></p> |
| * |
| * @param text the number to be formatted, will be modified with the formatting |
| */ |
| 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; |
| } |
| CharSequence saved = text.subSequence(0, length); |
| |
| // Strip the dashes first, as we're going to add them back |
| int p = 0; |
| while (p < text.length()) { |
| if (text.charAt(p) == '-') { |
| text.delete(p, p + 1); |
| } else { |
| p++; |
| } |
| } |
| 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: |
| * |
| * <p><code> |
| * 03-xxxx-xxxx |
| * 090-xxxx-xxxx |
| * 0120-xxx-xxx |
| * +81-3-xxxx-xxxx |
| * +81-90-xxxx-xxxx |
| * </code></p> |
| * |
| * @param text the number to be formatted, will be modified with |
| * the formatting |
| */ |
| public static void formatJapaneseNumber(Editable text) { |
| JapanesePhoneNumberFormatter.format(text); |
| } |
| |
| // Three and four digit phone numbers for either special services |
| // or from the network (eg carrier-originated SMS messages) should |
| // not match |
| static final int MIN_MATCH = 5; |
| |
| /** |
| * isEmergencyNumber: 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 if the number is in the list of emergency numbers |
| * listed in the ril / sim, then return true, otherwise false. |
| */ |
| public static boolean isEmergencyNumber(String number) { |
| // Strip the separators from the number before comparing it |
| // to the list. |
| number = extractNetworkPortion(number); |
| |
| // retrieve the list of emergency numbers |
| String numbers = SystemProperties.get("ro.ril.ecclist"); |
| |
| if (!TextUtils.isEmpty(numbers)) { |
| // searches through the comma-separated list for a match, |
| // return true if one is found. |
| for (String emergencyNum : numbers.split(",")) { |
| if (emergencyNum.equals(number)) { |
| return true; |
| } |
| } |
| // no matches found against the list! |
| return false; |
| } |
| |
| //no ecclist system property, so use our own list. |
| return (number.equals("112") || number.equals("911")); |
| } |
| |
| /** |
| * 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'); |
| } |
| } |