PhoneNumberUtils.java revision 3a08cec99e104f74f28ba2463f00c8d4e6d1967e
1/* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.telephony; 18 19import android.content.Context; 20import android.content.Intent; 21import android.database.Cursor; 22import android.net.Uri; 23import android.os.SystemProperties; 24import android.provider.Contacts; 25import android.text.Editable; 26import android.text.SpannableStringBuilder; 27import android.text.TextUtils; 28import android.util.SparseIntArray; 29 30import java.util.Locale; 31import java.util.regex.Matcher; 32import java.util.regex.Pattern; 33 34/** 35 * Various utilities for dealing with phone number strings. 36 */ 37public class PhoneNumberUtils 38{ 39 /* 40 * Special characters 41 * 42 * (See "What is a phone number?" doc) 43 * 'p' --- GSM pause character, same as comma 44 * 'n' --- GSM wild character 45 * 'w' --- GSM wait character 46 */ 47 public static final char PAUSE = ','; 48 public static final char WAIT = ';'; 49 public static final char WILD = 'N'; 50 51 /* 52 * TOA = TON + NPI 53 * See TS 24.008 section 10.5.4.7 for details. 54 * These are the only really useful TOA values 55 */ 56 public static final int TOA_International = 0x91; 57 public static final int TOA_Unknown = 0x81; 58 59 /* 60 * global-phone-number = ["+"] 1*( DIGIT / written-sep ) 61 * written-sep = ("-"/".") 62 */ 63 private static final Pattern GLOBAL_PHONE_NUMBER_PATTERN = 64 Pattern.compile("[\\+]?[0-9.-]+"); 65 66 /** True if c is ISO-LATIN characters 0-9 */ 67 public static boolean 68 isISODigit (char c) { 69 return c >= '0' && c <= '9'; 70 } 71 72 /** True if c is ISO-LATIN characters 0-9, *, # */ 73 public final static boolean 74 is12Key(char c) { 75 return (c >= '0' && c <= '9') || c == '*' || c == '#'; 76 } 77 78 /** True if c is ISO-LATIN characters 0-9, *, # , +, WILD */ 79 public final static boolean 80 isDialable(char c) { 81 return (c >= '0' && c <= '9') || c == '*' || c == '#' || c == '+' || c == WILD; 82 } 83 84 /** True if c is ISO-LATIN characters 0-9, *, # , + (no WILD) */ 85 public final static boolean 86 isReallyDialable(char c) { 87 return (c >= '0' && c <= '9') || c == '*' || c == '#' || c == '+'; 88 } 89 90 /** True if c is ISO-LATIN characters 0-9, *, # , +, WILD, WAIT, PAUSE */ 91 public final static boolean 92 isNonSeparator(char c) { 93 return (c >= '0' && c <= '9') || c == '*' || c == '#' || c == '+' 94 || c == WILD || c == WAIT || c == PAUSE; 95 } 96 97 /** This any anything to the right of this char is part of the 98 * post-dial string (eg this is PAUSE or WAIT) 99 */ 100 public final static boolean 101 isStartsPostDial (char c) { 102 return c == PAUSE || c == WAIT; 103 } 104 105 /** Extracts the phone number from an Intent. 106 * 107 * @param intent the intent to get the number of 108 * @param context a context to use for database access 109 * 110 * @return the phone number that would be called by the intent, or 111 * <code>null</code> if the number cannot be found. 112 */ 113 public static String getNumberFromIntent(Intent intent, Context context) { 114 String number = null; 115 116 Uri uri = intent.getData(); 117 String scheme = uri.getScheme(); 118 119 if (scheme.equals("tel")) { 120 return uri.getSchemeSpecificPart(); 121 } 122 123 if (scheme.equals("voicemail")) { 124 return TelephonyManager.getDefault().getVoiceMailNumber(); 125 } 126 127 if (context == null) { 128 return null; 129 } 130 131 String type = intent.resolveType(context); 132 133 Cursor c = context.getContentResolver().query( 134 uri, new String[]{ Contacts.People.Phones.NUMBER }, 135 null, null, null); 136 if (c != null) { 137 try { 138 if (c.moveToFirst()) { 139 number = c.getString( 140 c.getColumnIndex(Contacts.People.Phones.NUMBER)); 141 } 142 } finally { 143 c.close(); 144 } 145 } 146 147 return number; 148 } 149 150 /** Extracts the network address portion and canonicalizes 151 * (filters out separators.) 152 * Network address portion is everything up to DTMF control digit 153 * separators (pause or wait), but without non-dialable characters. 154 * 155 * Please note that the GSM wild character is allowed in the result. 156 * This must be resolved before dialing. 157 * 158 * Allows + only in the first position in the result string. 159 * 160 * Returns null if phoneNumber == null 161 */ 162 public static String 163 extractNetworkPortion(String phoneNumber) { 164 if (phoneNumber == null) { 165 return null; 166 } 167 168 int len = phoneNumber.length(); 169 StringBuilder ret = new StringBuilder(len); 170 boolean firstCharAdded = false; 171 172 for (int i = 0; i < len; i++) { 173 char c = phoneNumber.charAt(i); 174 if (isDialable(c) && (c != '+' || !firstCharAdded)) { 175 firstCharAdded = true; 176 ret.append(c); 177 } else if (isStartsPostDial (c)) { 178 break; 179 } 180 } 181 182 return ret.toString(); 183 } 184 185 /** 186 * Strips separators from a phone number string. 187 * @param phoneNumber phone number to strip. 188 * @return phone string stripped of separators. 189 */ 190 public static String stripSeparators(String phoneNumber) { 191 if (phoneNumber == null) { 192 return null; 193 } 194 int len = phoneNumber.length(); 195 StringBuilder ret = new StringBuilder(len); 196 197 for (int i = 0; i < len; i++) { 198 char c = phoneNumber.charAt(i); 199 if (isNonSeparator(c)) { 200 ret.append(c); 201 } 202 } 203 204 return ret.toString(); 205 } 206 207 /** or -1 if both are negative */ 208 static private int 209 minPositive (int a, int b) { 210 if (a >= 0 && b >= 0) { 211 return (a < b) ? a : b; 212 } else if (a >= 0) { /* && b < 0 */ 213 return a; 214 } else if (b >= 0) { /* && a < 0 */ 215 return b; 216 } else { /* a < 0 && b < 0 */ 217 return -1; 218 } 219 } 220 221 /** index of the last character of the network portion 222 * (eg anything after is a post-dial string) 223 */ 224 static private int 225 indexOfLastNetworkChar(String a) { 226 int pIndex, wIndex; 227 int origLength; 228 int trimIndex; 229 230 origLength = a.length(); 231 232 pIndex = a.indexOf(PAUSE); 233 wIndex = a.indexOf(WAIT); 234 235 trimIndex = minPositive(pIndex, wIndex); 236 237 if (trimIndex < 0) { 238 return origLength - 1; 239 } else { 240 return trimIndex - 1; 241 } 242 } 243 244 /** 245 * Extracts the post-dial sequence of DTMF control digits, pauses, and 246 * waits. Strips separators. This string may be empty, but will not be null 247 * unless phoneNumber == null. 248 * 249 * Returns null if phoneNumber == null 250 */ 251 252 public static String 253 extractPostDialPortion(String phoneNumber) { 254 if (phoneNumber == null) return null; 255 256 int trimIndex; 257 StringBuilder ret = new StringBuilder(); 258 259 trimIndex = indexOfLastNetworkChar (phoneNumber); 260 261 for (int i = trimIndex + 1, s = phoneNumber.length() 262 ; i < s; i++ 263 ) { 264 char c = phoneNumber.charAt(i); 265 if (isNonSeparator(c)) { 266 ret.append(c); 267 } 268 } 269 270 return ret.toString(); 271 } 272 273 /** 274 * Compare phone numbers a and b, return true if they're identical 275 * enough for caller ID purposes. 276 * 277 * - Compares from right to left 278 * - requires MIN_MATCH (5) characters to match 279 * - handles common trunk prefixes and international prefixes 280 * (basically, everything except the Russian trunk prefix) 281 * 282 * Tolerates nulls 283 */ 284 public static boolean 285 compare(String a, String b) { 286 int ia, ib; 287 int matched; 288 289 if (a == null || b == null) return a == b; 290 291 if (a.length() == 0 || b.length() == 0) { 292 return false; 293 } 294 295 ia = indexOfLastNetworkChar (a); 296 ib = indexOfLastNetworkChar (b); 297 matched = 0; 298 299 while (ia >= 0 && ib >=0) { 300 char ca, cb; 301 boolean skipCmp = false; 302 303 ca = a.charAt(ia); 304 305 if (!isDialable(ca)) { 306 ia--; 307 skipCmp = true; 308 } 309 310 cb = b.charAt(ib); 311 312 if (!isDialable(cb)) { 313 ib--; 314 skipCmp = true; 315 } 316 317 if (!skipCmp) { 318 if (cb != ca && ca != WILD && cb != WILD) { 319 break; 320 } 321 ia--; ib--; matched++; 322 } 323 } 324 325 if (matched < MIN_MATCH) { 326 int aLen = a.length(); 327 328 // if the input strings match, but their lengths < MIN_MATCH, 329 // treat them as equal. 330 if (aLen == b.length() && aLen == matched) { 331 return true; 332 } 333 return false; 334 } 335 336 // At least one string has matched completely; 337 if (matched >= MIN_MATCH && (ia < 0 || ib < 0)) { 338 return true; 339 } 340 341 /* 342 * Now, what remains must be one of the following for a 343 * match: 344 * 345 * - a '+' on one and a '00' or a '011' on the other 346 * - a '0' on one and a (+,00)<country code> on the other 347 * (for this, a '0' and a '00' prefix would have succeeded above) 348 */ 349 350 if (matchIntlPrefix(a, ia + 1) 351 && matchIntlPrefix (b, ib +1) 352 ) { 353 return true; 354 } 355 356 if (matchTrunkPrefix(a, ia + 1) 357 && matchIntlPrefixAndCC(b, ib +1) 358 ) { 359 return true; 360 } 361 362 if (matchTrunkPrefix(b, ib + 1) 363 && matchIntlPrefixAndCC(a, ia +1) 364 ) { 365 return true; 366 } 367 368 return false; 369 } 370 371 /** 372 * Returns the rightmost MIN_MATCH (5) characters in the network portion 373 * in *reversed* order 374 * 375 * This can be used to do a database lookup against the column 376 * that stores getStrippedReversed() 377 * 378 * Returns null if phoneNumber == null 379 */ 380 public static String 381 toCallerIDMinMatch(String phoneNumber) { 382 String np = extractNetworkPortion(phoneNumber); 383 return internalGetStrippedReversed(np, MIN_MATCH); 384 } 385 386 /** 387 * Returns the network portion reversed. 388 * This string is intended to go into an index column for a 389 * database lookup. 390 * 391 * Returns null if phoneNumber == null 392 */ 393 public static String 394 getStrippedReversed(String phoneNumber) { 395 String np = extractNetworkPortion(phoneNumber); 396 397 if (np == null) return null; 398 399 return internalGetStrippedReversed(np, np.length()); 400 } 401 402 /** 403 * Returns the last numDigits of the reversed phone number 404 * Returns null if np == null 405 */ 406 private static String 407 internalGetStrippedReversed(String np, int numDigits) { 408 if (np == null) return null; 409 410 StringBuilder ret = new StringBuilder(numDigits); 411 int length = np.length(); 412 413 for (int i = length - 1, s = length 414 ; i >= 0 && (s - i) <= numDigits ; i-- 415 ) { 416 char c = np.charAt(i); 417 418 ret.append(c); 419 } 420 421 return ret.toString(); 422 } 423 424 /** 425 * Basically: makes sure there's a + in front of a 426 * TOA_International number 427 * 428 * Returns null if s == null 429 */ 430 public static String 431 stringFromStringAndTOA(String s, int TOA) { 432 if (s == null) return null; 433 434 if (TOA == TOA_International && s.length() > 0 && s.charAt(0) != '+') { 435 return "+" + s; 436 } 437 438 return s; 439 } 440 441 /** 442 * Returns the TOA for the given dial string 443 * Basically, returns TOA_International if there's a + prefix 444 */ 445 446 public static int 447 toaFromString(String s) { 448 if (s != null && s.length() > 0 && s.charAt(0) == '+') { 449 return TOA_International; 450 } 451 452 return TOA_Unknown; 453 } 454 455 /** 456 * Phone numbers are stored in "lookup" form in the database 457 * as reversed strings to allow for caller ID lookup 458 * 459 * This method takes a phone number and makes a valid SQL "LIKE" 460 * string that will match the lookup form 461 * 462 */ 463 /** all of a up to len must be an international prefix or 464 * separators/non-dialing digits 465 */ 466 private static boolean 467 matchIntlPrefix(String a, int len) { 468 /* '([^0-9*#+pwn]\+[^0-9*#+pwn] | [^0-9*#+pwn]0(0|11)[^0-9*#+pwn] )$' */ 469 /* 0 1 2 3 45 */ 470 471 int state = 0; 472 for (int i = 0 ; i < len ; i++) { 473 char c = a.charAt(i); 474 475 switch (state) { 476 case 0: 477 if (c == '+') state = 1; 478 else if (c == '0') state = 2; 479 else if (isNonSeparator(c)) return false; 480 break; 481 482 case 2: 483 if (c == '0') state = 3; 484 else if (c == '1') state = 4; 485 else if (isNonSeparator(c)) return false; 486 break; 487 488 case 4: 489 if (c == '1') state = 5; 490 else if (isNonSeparator(c)) return false; 491 break; 492 493 default: 494 if (isNonSeparator(c)) return false; 495 break; 496 497 } 498 } 499 500 return state == 1 || state == 3 || state == 5; 501 } 502 503 /** 504 * 3GPP TS 24.008 10.5.4.7 505 * Called Party BCD Number 506 * 507 * See Also TS 51.011 10.5.1 "dialing number/ssc string" 508 * and TS 11.11 "10.3.1 EF adn (Abbreviated dialing numbers)" 509 * 510 * @param bytes the data buffer 511 * @param offset should point to the TOA (aka. TON/NPI) octet after the length byte 512 * @param length is the number of bytes including TOA byte 513 * and must be at least 2 514 * 515 * @return partial string on invalid decode 516 * 517 * FIXME(mkf) support alphanumeric address type 518 * currently implemented in SMSMessage.getAddress() 519 */ 520 public static String 521 calledPartyBCDToString (byte[] bytes, int offset, int length) { 522 boolean prependPlus = false; 523 StringBuilder ret = new StringBuilder(1 + length * 2); 524 525 if (length < 2) { 526 return ""; 527 } 528 529 if ((bytes[offset] & 0xff) == TOA_International) { 530 prependPlus = true; 531 } 532 533 internalCalledPartyBCDFragmentToString( 534 ret, bytes, offset + 1, length - 1); 535 536 if (prependPlus && ret.length() == 0) { 537 // If the only thing there is a prepended plus, return "" 538 return ""; 539 } 540 541 if (prependPlus) { 542 // This is an "international number" and should have 543 // a plus prepended to the dialing number. But there 544 // can also be Gsm MMI codes as defined in TS 22.030 6.5.2 545 // so we need to handle those also. 546 // 547 // http://web.telia.com/~u47904776/gsmkode.htm is a 548 // has a nice list of some of these GSM codes. 549 // 550 // Examples are: 551 // **21*+886988171479# 552 // **21*8311234567# 553 // *21# 554 // #21# 555 // *#21# 556 // *31#+11234567890 557 // #31#+18311234567 558 // #31#8311234567 559 // 18311234567 560 // +18311234567# 561 // +18311234567 562 // Odd ball cases that some phones handled 563 // where there is no dialing number so they 564 // append the "+" 565 // *21#+ 566 // **21#+ 567 String retString = ret.toString(); 568 Pattern p = Pattern.compile("(^[#*])(.*)([#*])(.*)(#)$"); 569 Matcher m = p.matcher(retString); 570 if (m.matches()) { 571 if ("".equals(m.group(2))) { 572 // Started with two [#*] ends with # 573 // So no dialing number and we'll just 574 // append a +, this handles **21#+ 575 ret = new StringBuilder(); 576 ret.append(m.group(1)); 577 ret.append(m.group(3)); 578 ret.append(m.group(4)); 579 ret.append(m.group(5)); 580 ret.append("+"); 581 } else { 582 // Starts with [#*] and ends with # 583 // Assume group 4 is a dialing number 584 // such as *21*+1234554# 585 ret = new StringBuilder(); 586 ret.append(m.group(1)); 587 ret.append(m.group(2)); 588 ret.append(m.group(3)); 589 ret.append("+"); 590 ret.append(m.group(4)); 591 ret.append(m.group(5)); 592 } 593 } else { 594 p = Pattern.compile("(^[#*])(.*)([#*])(.*)"); 595 m = p.matcher(retString); 596 if (m.matches()) { 597 // Starts with [#*] and only one other [#*] 598 // Assume the data after last [#*] is dialing 599 // number (i.e. group 4) such as *31#+11234567890. 600 // This also includes the odd ball *21#+ 601 ret = new StringBuilder(); 602 ret.append(m.group(1)); 603 ret.append(m.group(2)); 604 ret.append(m.group(3)); 605 ret.append("+"); 606 ret.append(m.group(4)); 607 } else { 608 // Does NOT start with [#*] just prepend '+' 609 ret = new StringBuilder(); 610 ret.append('+'); 611 ret.append(retString); 612 } 613 } 614 } 615 616 return ret.toString(); 617 } 618 619 private static void 620 internalCalledPartyBCDFragmentToString( 621 StringBuilder sb, byte [] bytes, int offset, int length) { 622 for (int i = offset ; i < length + offset ; i++) { 623 byte b; 624 char c; 625 626 c = bcdToChar((byte)(bytes[i] & 0xf)); 627 628 if (c == 0) { 629 return; 630 } 631 sb.append(c); 632 633 // FIXME(mkf) TS 23.040 9.1.2.3 says 634 // "if a mobile receives 1111 in a position prior to 635 // the last semi-octet then processing shall commense with 636 // the next semi-octet and the intervening 637 // semi-octet shall be ignored" 638 // How does this jive with 24,008 10.5.4.7 639 640 b = (byte)((bytes[i] >> 4) & 0xf); 641 642 if (b == 0xf && i + 1 == length + offset) { 643 //ignore final 0xf 644 break; 645 } 646 647 c = bcdToChar(b); 648 if (c == 0) { 649 return; 650 } 651 652 sb.append(c); 653 } 654 655 } 656 657 /** 658 * Like calledPartyBCDToString, but field does not start with a 659 * TOA byte. For example: SIM ADN extension fields 660 */ 661 662 public static String 663 calledPartyBCDFragmentToString(byte [] bytes, int offset, int length) { 664 StringBuilder ret = new StringBuilder(length * 2); 665 666 internalCalledPartyBCDFragmentToString(ret, bytes, offset, length); 667 668 return ret.toString(); 669 } 670 671 /** returns 0 on invalid value */ 672 private static char 673 bcdToChar(byte b) { 674 if (b < 0xa) { 675 return (char)('0' + b); 676 } else switch (b) { 677 case 0xa: return '*'; 678 case 0xb: return '#'; 679 case 0xc: return PAUSE; 680 case 0xd: return WILD; 681 682 default: return 0; 683 } 684 } 685 686 private static int 687 charToBCD(char c) { 688 if (c >= '0' && c <= '9') { 689 return c - '0'; 690 } else if (c == '*') { 691 return 0xa; 692 } else if (c == '#') { 693 return 0xb; 694 } else if (c == PAUSE) { 695 return 0xc; 696 } else if (c == WILD) { 697 return 0xd; 698 } else { 699 throw new RuntimeException ("invalid char for BCD " + c); 700 } 701 } 702 703 /** 704 * Return true iff the network portion of <code>address</code> is, 705 * as far as we can tell on the device, suitable for use as an SMS 706 * destination address. 707 */ 708 public static boolean isWellFormedSmsAddress(String address) { 709 String networkPortion = 710 PhoneNumberUtils.extractNetworkPortion(address); 711 712 return (!(networkPortion.equals("+") 713 || TextUtils.isEmpty(networkPortion))) 714 && isDialable(networkPortion); 715 } 716 717 public static boolean isGlobalPhoneNumber(String phoneNumber) { 718 if (TextUtils.isEmpty(phoneNumber)) { 719 return false; 720 } 721 722 Matcher match = GLOBAL_PHONE_NUMBER_PATTERN.matcher(phoneNumber); 723 return match.matches(); 724 } 725 726 private static boolean isDialable(String address) { 727 for (int i = 0, count = address.length(); i < count; i++) { 728 if (!isDialable(address.charAt(i))) { 729 return false; 730 } 731 } 732 return true; 733 } 734 735 /** 736 * Note: calls extractNetworkPortion(), so do not use for 737 * SIM EF[ADN] style records 738 * 739 * Returns null if network portion is empty. 740 */ 741 public static byte[] 742 networkPortionToCalledPartyBCD(String s) { 743 String networkPortion = extractNetworkPortion(s); 744 return numberToCalledPartyBCDHelper(networkPortion, false); 745 } 746 747 /** 748 * Same as {@link #networkPortionToCalledPartyBCD}, but includes a 749 * one-byte length prefix. 750 */ 751 public static byte[] 752 networkPortionToCalledPartyBCDWithLength(String s) { 753 String networkPortion = extractNetworkPortion(s); 754 return numberToCalledPartyBCDHelper(networkPortion, true); 755 } 756 757 /** 758 * Convert a dialing number to BCD byte array 759 * 760 * @param number dialing number string 761 * if the dialing number starts with '+', set to internationl TOA 762 * @return BCD byte array 763 */ 764 public static byte[] 765 numberToCalledPartyBCD(String number) { 766 return numberToCalledPartyBCDHelper(number, false); 767 } 768 769 /** 770 * If includeLength is true, prepend a one-byte length value to 771 * the return array. 772 */ 773 private static byte[] 774 numberToCalledPartyBCDHelper(String number, boolean includeLength) { 775 int numberLenReal = number.length(); 776 int numberLenEffective = numberLenReal; 777 boolean hasPlus = number.indexOf('+') != -1; 778 if (hasPlus) numberLenEffective--; 779 780 if (numberLenEffective == 0) return null; 781 782 int resultLen = (numberLenEffective + 1) / 2; // Encoded numbers require only 4 bits each. 783 int extraBytes = 1; // Prepended TOA byte. 784 if (includeLength) extraBytes++; // Optional prepended length byte. 785 resultLen += extraBytes; 786 787 byte[] result = new byte[resultLen]; 788 789 int digitCount = 0; 790 for (int i = 0; i < numberLenReal; i++) { 791 char c = number.charAt(i); 792 if (c == '+') continue; 793 int shift = ((digitCount & 0x01) == 1) ? 4 : 0; 794 result[extraBytes + (digitCount >> 1)] |= (byte)((charToBCD(c) & 0x0F) << shift); 795 digitCount++; 796 } 797 798 // 1-fill any trailing odd nibble/quartet. 799 if ((digitCount & 0x01) == 1) result[extraBytes + (digitCount >> 1)] |= 0xF0; 800 801 int offset = 0; 802 if (includeLength) result[offset++] = (byte)(resultLen - 1); 803 result[offset] = (byte)(hasPlus ? TOA_International : TOA_Unknown); 804 805 return result; 806 } 807 808 /** all of 'a' up to len must match non-US trunk prefix ('0') */ 809 private static boolean 810 matchTrunkPrefix(String a, int len) { 811 boolean found; 812 813 found = false; 814 815 for (int i = 0 ; i < len ; i++) { 816 char c = a.charAt(i); 817 818 if (c == '0' && !found) { 819 found = true; 820 } else if (isNonSeparator(c)) { 821 return false; 822 } 823 } 824 825 return found; 826 } 827 828 /** all of 'a' up to len must be a (+|00|011)country code) 829 * We're fast and loose with the country code. Any \d{1,3} matches */ 830 private static boolean 831 matchIntlPrefixAndCC(String a, int len) { 832 /* [^0-9*#+pwn]*(\+|0(0|11)\d\d?\d? [^0-9*#+pwn] $ */ 833 /* 0 1 2 3 45 6 7 8 */ 834 835 int state = 0; 836 for (int i = 0 ; i < len ; i++ ) { 837 char c = a.charAt(i); 838 839 switch (state) { 840 case 0: 841 if (c == '+') state = 1; 842 else if (c == '0') state = 2; 843 else if (isNonSeparator(c)) return false; 844 break; 845 846 case 2: 847 if (c == '0') state = 3; 848 else if (c == '1') state = 4; 849 else if (isNonSeparator(c)) return false; 850 break; 851 852 case 4: 853 if (c == '1') state = 5; 854 else if (isNonSeparator(c)) return false; 855 break; 856 857 case 1: 858 case 3: 859 case 5: 860 if (isISODigit(c)) state = 6; 861 else if (isNonSeparator(c)) return false; 862 break; 863 864 case 6: 865 case 7: 866 if (isISODigit(c)) state++; 867 else if (isNonSeparator(c)) return false; 868 break; 869 870 default: 871 if (isNonSeparator(c)) return false; 872 } 873 } 874 875 return state == 6 || state == 7 || state == 8; 876 } 877 878 //================ Number formatting ========================= 879 880 /** The current locale is unknown, look for a country code or don't format */ 881 public static final int FORMAT_UNKNOWN = 0; 882 /** NANP formatting */ 883 public static final int FORMAT_NANP = 1; 884 /** Japanese formatting */ 885 public static final int FORMAT_JAPAN = 2; 886 887 /** List of country codes for countries that use the NANP */ 888 private static final String[] NANP_COUNTRIES = new String[] { 889 "US", // United States 890 "CA", // Canada 891 "AS", // American Samoa 892 "AI", // Anguilla 893 "AG", // Antigua and Barbuda 894 "BS", // Bahamas 895 "BB", // Barbados 896 "BM", // Bermuda 897 "VG", // British Virgin Islands 898 "KY", // Cayman Islands 899 "DM", // Dominica 900 "DO", // Dominican Republic 901 "GD", // Grenada 902 "GU", // Guam 903 "JM", // Jamaica 904 "PR", // Puerto Rico 905 "MS", // Montserrat 906 "NP", // Northern Mariana Islands 907 "KN", // Saint Kitts and Nevis 908 "LC", // Saint Lucia 909 "VC", // Saint Vincent and the Grenadines 910 "TT", // Trinidad and Tobago 911 "TC", // Turks and Caicos Islands 912 "VI", // U.S. Virgin Islands 913 }; 914 915 /** 916 * Breaks the given number down and formats it according to the rules 917 * for the country the number is from. 918 * 919 * @param source the phone number to format 920 * @return a locally acceptable formatting of the input, or the raw input if 921 * formatting rules aren't known for the number 922 */ 923 public static String formatNumber(String source) { 924 SpannableStringBuilder text = new SpannableStringBuilder(source); 925 formatNumber(text, getFormatTypeForLocale(Locale.getDefault())); 926 return text.toString(); 927 } 928 929 /** 930 * Returns the phone number formatting type for the given locale. 931 * 932 * @param locale The locale of interest, usually {@link Locale#getDefault()} 933 * @return the formatting type for the given locale, or FORMAT_UNKNOWN if the formatting 934 * rules are not known for the given locale 935 */ 936 public static int getFormatTypeForLocale(Locale locale) { 937 String country = locale.getCountry(); 938 939 // Check for the NANP countries 940 int length = NANP_COUNTRIES.length; 941 for (int i = 0; i < length; i++) { 942 if (NANP_COUNTRIES[i].equals(country)) { 943 return FORMAT_NANP; 944 } 945 } 946 if (locale.equals(Locale.JAPAN)) { 947 return FORMAT_JAPAN; 948 } 949 return FORMAT_UNKNOWN; 950 } 951 952 /** 953 * Formats a phone number in-place. Currently only supports NANP formatting. 954 * 955 * @param text The number to be formatted, will be modified with the formatting 956 * @param defaultFormattingType The default formatting rules to apply if the number does 957 * not begin with +<country_code> 958 */ 959 public static void formatNumber(Editable text, int defaultFormattingType) { 960 int formatType = defaultFormattingType; 961 962 if (text.length() > 2 && text.charAt(0) == '+') { 963 if (text.charAt(1) == '1') { 964 formatType = FORMAT_NANP; 965 } else if (text.length() >= 3 && text.charAt(1) == '8' 966 && text.charAt(2) == '1') { 967 formatType = FORMAT_JAPAN; 968 } else { 969 return; 970 } 971 } 972 973 switch (formatType) { 974 case FORMAT_NANP: 975 formatNanpNumber(text); 976 return; 977 case FORMAT_JAPAN: 978 formatJapaneseNumber(text); 979 return; 980 } 981 } 982 983 private static final int NANP_STATE_DIGIT = 1; 984 private static final int NANP_STATE_PLUS = 2; 985 private static final int NANP_STATE_ONE = 3; 986 private static final int NANP_STATE_DASH = 4; 987 988 /** 989 * Formats a phone number in-place using the NANP formatting rules. Numbers will be formatted 990 * as: 991 * 992 * <p><code> 993 * xxx-xxxx 994 * xxx-xxx-xxxx 995 * 1-xxx-xxx-xxxx 996 * +1-xxx-xxx-xxxx 997 * </code></p> 998 * 999 * @param text the number to be formatted, will be modified with the formatting 1000 */ 1001 public static void formatNanpNumber(Editable text) { 1002 int length = text.length(); 1003 if (length > "+1-nnn-nnn-nnnn".length()) { 1004 // The string is too long to be formatted 1005 return; 1006 } 1007 CharSequence saved = text.subSequence(0, length); 1008 1009 // Strip the dashes first, as we're going to add them back 1010 int p = 0; 1011 while (p < text.length()) { 1012 if (text.charAt(p) == '-') { 1013 text.delete(p, p + 1); 1014 } else { 1015 p++; 1016 } 1017 } 1018 length = text.length(); 1019 1020 // When scanning the number we record where dashes need to be added, 1021 // if they're non-0 at the end of the scan the dashes will be added in 1022 // the proper places. 1023 int dashPositions[] = new int[3]; 1024 int numDashes = 0; 1025 1026 int state = NANP_STATE_DIGIT; 1027 int numDigits = 0; 1028 for (int i = 0; i < length; i++) { 1029 char c = text.charAt(i); 1030 switch (c) { 1031 case '1': 1032 if (numDigits == 0 || state == NANP_STATE_PLUS) { 1033 state = NANP_STATE_ONE; 1034 break; 1035 } 1036 // fall through 1037 case '2': 1038 case '3': 1039 case '4': 1040 case '5': 1041 case '6': 1042 case '7': 1043 case '8': 1044 case '9': 1045 case '0': 1046 if (state == NANP_STATE_PLUS) { 1047 // Only NANP number supported for now 1048 text.replace(0, length, saved); 1049 return; 1050 } else if (state == NANP_STATE_ONE) { 1051 // Found either +1 or 1, follow it up with a dash 1052 dashPositions[numDashes++] = i; 1053 } else if (state != NANP_STATE_DASH && (numDigits == 3 || numDigits == 6)) { 1054 // Found a digit that should be after a dash that isn't 1055 dashPositions[numDashes++] = i; 1056 } 1057 state = NANP_STATE_DIGIT; 1058 numDigits++; 1059 break; 1060 1061 case '-': 1062 state = NANP_STATE_DASH; 1063 break; 1064 1065 case '+': 1066 if (i == 0) { 1067 // Plus is only allowed as the first character 1068 state = NANP_STATE_PLUS; 1069 break; 1070 } 1071 // Fall through 1072 default: 1073 // Unknown character, bail on formatting 1074 text.replace(0, length, saved); 1075 return; 1076 } 1077 } 1078 1079 if (numDigits == 7) { 1080 // With 7 digits we want xxx-xxxx, not xxx-xxx-x 1081 numDashes--; 1082 } 1083 1084 // Actually put the dashes in place 1085 for (int i = 0; i < numDashes; i++) { 1086 int pos = dashPositions[i]; 1087 text.replace(pos + i, pos + i, "-"); 1088 } 1089 1090 // Remove trailing dashes 1091 int len = text.length(); 1092 while (len > 0) { 1093 if (text.charAt(len - 1) == '-') { 1094 text.delete(len - 1, len); 1095 len--; 1096 } else { 1097 break; 1098 } 1099 } 1100 } 1101 1102 /** 1103 * Formats a phone number in-place using the Japanese formatting rules. 1104 * Numbers will be formatted as: 1105 * 1106 * <p><code> 1107 * 03-xxxx-xxxx 1108 * 090-xxxx-xxxx 1109 * 0120-xxx-xxx 1110 * +81-3-xxxx-xxxx 1111 * +81-90-xxxx-xxxx 1112 * </code></p> 1113 * 1114 * @param text the number to be formatted, will be modified with 1115 * the formatting 1116 */ 1117 public static void formatJapaneseNumber(Editable text) { 1118 JapanesePhoneNumberFormatter.format(text); 1119 } 1120 1121 // Three and four digit phone numbers for either special services 1122 // or from the network (eg carrier-originated SMS messages) should 1123 // not match 1124 static final int MIN_MATCH = 5; 1125 1126 /** 1127 * isEmergencyNumber: checks a given number against the list of 1128 * emergency numbers provided by the RIL and SIM card. 1129 * 1130 * @param number the number to look up. 1131 * @return if the number is in the list of emergency numbers 1132 * listed in the ril / sim, then return true, otherwise false. 1133 */ 1134 public static boolean isEmergencyNumber(String number) { 1135 // Strip the separators from the number before comparing it 1136 // to the list. 1137 number = extractNetworkPortion(number); 1138 1139 // retrieve the list of emergency numbers 1140 String numbers = SystemProperties.get("ro.ril.ecclist"); 1141 1142 if (!TextUtils.isEmpty(numbers)) { 1143 // searches through the comma-separated list for a match, 1144 // return true if one is found. 1145 for (String emergencyNum : numbers.split(",")) { 1146 if (emergencyNum.equals(number)) { 1147 return true; 1148 } 1149 } 1150 // no matches found against the list! 1151 return false; 1152 } 1153 1154 //no ecclist system property, so use our own list. 1155 return (number.equals("112") || number.equals("911")); 1156 } 1157 1158 /** 1159 * Translates any alphabetic letters (i.e. [A-Za-z]) in the 1160 * specified phone number into the equivalent numeric digits, 1161 * according to the phone keypad letter mapping described in 1162 * ITU E.161 and ISO/IEC 9995-8. 1163 * 1164 * @return the input string, with alpha letters converted to numeric 1165 * digits using the phone keypad letter mapping. For example, 1166 * an input of "1-800-GOOG-411" will return "1-800-4664-411". 1167 */ 1168 public static String convertKeypadLettersToDigits(String input) { 1169 if (input == null) { 1170 return input; 1171 } 1172 int len = input.length(); 1173 if (len == 0) { 1174 return input; 1175 } 1176 1177 char[] out = input.toCharArray(); 1178 1179 for (int i = 0; i < len; i++) { 1180 char c = out[i]; 1181 // If this char isn't in KEYPAD_MAP at all, just leave it alone. 1182 out[i] = (char) KEYPAD_MAP.get(c, c); 1183 } 1184 1185 return new String(out); 1186 } 1187 1188 /** 1189 * The phone keypad letter mapping (see ITU E.161 or ISO/IEC 9995-8.) 1190 * TODO: This should come from a resource. 1191 */ 1192 private static final SparseIntArray KEYPAD_MAP = new SparseIntArray(); 1193 static { 1194 KEYPAD_MAP.put('a', '2'); KEYPAD_MAP.put('b', '2'); KEYPAD_MAP.put('c', '2'); 1195 KEYPAD_MAP.put('A', '2'); KEYPAD_MAP.put('B', '2'); KEYPAD_MAP.put('C', '2'); 1196 1197 KEYPAD_MAP.put('d', '3'); KEYPAD_MAP.put('e', '3'); KEYPAD_MAP.put('f', '3'); 1198 KEYPAD_MAP.put('D', '3'); KEYPAD_MAP.put('E', '3'); KEYPAD_MAP.put('F', '3'); 1199 1200 KEYPAD_MAP.put('g', '4'); KEYPAD_MAP.put('h', '4'); KEYPAD_MAP.put('i', '4'); 1201 KEYPAD_MAP.put('G', '4'); KEYPAD_MAP.put('H', '4'); KEYPAD_MAP.put('I', '4'); 1202 1203 KEYPAD_MAP.put('j', '5'); KEYPAD_MAP.put('k', '5'); KEYPAD_MAP.put('l', '5'); 1204 KEYPAD_MAP.put('J', '5'); KEYPAD_MAP.put('K', '5'); KEYPAD_MAP.put('L', '5'); 1205 1206 KEYPAD_MAP.put('m', '6'); KEYPAD_MAP.put('n', '6'); KEYPAD_MAP.put('o', '6'); 1207 KEYPAD_MAP.put('M', '6'); KEYPAD_MAP.put('N', '6'); KEYPAD_MAP.put('O', '6'); 1208 1209 KEYPAD_MAP.put('p', '7'); KEYPAD_MAP.put('q', '7'); KEYPAD_MAP.put('r', '7'); KEYPAD_MAP.put('s', '7'); 1210 KEYPAD_MAP.put('P', '7'); KEYPAD_MAP.put('Q', '7'); KEYPAD_MAP.put('R', '7'); KEYPAD_MAP.put('S', '7'); 1211 1212 KEYPAD_MAP.put('t', '8'); KEYPAD_MAP.put('u', '8'); KEYPAD_MAP.put('v', '8'); 1213 KEYPAD_MAP.put('T', '8'); KEYPAD_MAP.put('U', '8'); KEYPAD_MAP.put('V', '8'); 1214 1215 KEYPAD_MAP.put('w', '9'); KEYPAD_MAP.put('x', '9'); KEYPAD_MAP.put('y', '9'); KEYPAD_MAP.put('z', '9'); 1216 KEYPAD_MAP.put('W', '9'); KEYPAD_MAP.put('X', '9'); KEYPAD_MAP.put('Y', '9'); KEYPAD_MAP.put('Z', '9'); 1217 } 1218} 1219