/* * 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 uk.ac.cam.db538.cryptosms.utils; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Various utilities for dealing with phone number strings. */ public class PhoneNumber { static final int MIN_MATCH = 7; /* * 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; static final String LOG_TAG = "PhoneNumberUtils"; /* * 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. * * @param c the c * @return true, if is iSO digit */ public static boolean isISODigit (char c) { return c >= '0' && c <= '9'; } /** * True if c is ISO-LATIN characters 0-9, *, #. * * @param c the c * @return true, if is 12 key */ public final static boolean is12Key(char c) { return (c >= '0' && c <= '9') || c == '*' || c == '#'; } /** * True if c is ISO-LATIN characters 0-9, *, # , +, WILD. * * @param c the c * @return true, if is dialable */ 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). * * @param c the c * @return true, if is really dialable */ 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. * * @param c the c * @return true, if is non separator */ 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). * * @param c the c * @return true, if is starts post dial */ public final static boolean isStartsPostDial (char c) { return c == PAUSE || c == WAIT; } /** 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')); } /** * 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; } } /** * Checks if is global phone number. * * @param phoneNumber the phone number * @return true, if is global phone number */ public static boolean isGlobalPhoneNumber(String phoneNumber) { if (phoneNumber == null || phoneNumber.length() == 0) { return false; } Matcher match = GLOBAL_PHONE_NUMBER_PATTERN.matcher(phoneNumber); return match.matches(); } /** * Compare phone numbers a and b, return true if they're identical enough for caller ID purposes. * * @param a the a * @param b the b * @return true, if successful */ 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 two phone numbers * * @param a the a * @param b the b * @param useStrictComparation the use strict comparation * @return true, if successful * @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()". * * @param a the a * @param b the b * @return true, if successful * @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)<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; } /** * Compare strictly. * * @param a the a * @param b the b * @return true, if successful * @hide */ public static boolean compareStrictly(String a, String b) { return compareStrictly(a, b, true); } /** * Compare strictly. * * @param a the a * @param b the b * @param acceptInvalidCCCPrefix the accept invalid ccc prefix * @return true, if successful * @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 equalt 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; } //===== Begining of utility methods used in compareLoosely() ===== /** * 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; } /** 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; } /** 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; } //===== End of utility methods used only in compareLoosely() ===== //===== Beggining of utility methods used only in compareStrictly() ==== /* * If true, the number is country calling code. */ private static final boolean COUNTLY_CALLING_CALL[] = { true, true, false, false, false, false, false, true, false, false, false, false, false, false, false, false, false, false, false, false, true, false, false, false, false, false, false, true, true, false, true, true, true, true, true, false, true, false, false, true, true, false, false, true, true, true, true, true, true, true, false, true, true, true, true, true, true, true, true, false, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, true, true, true, true, false, true, false, false, true, true, true, true, true, true, true, false, false, true, false, }; private static final int CCC_LENGTH = COUNTLY_CALLING_CALL.length; /** * @return true when input is valid Country Calling Code. */ private static boolean isCountryCallingCode(int countryCallingCodeCandidate) { return countryCallingCodeCandidate > 0 && countryCallingCodeCandidate < CCC_LENGTH && COUNTLY_CALLING_CALL[countryCallingCodeCandidate]; } /** * Returns interger 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 separater 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; } //==== End of utility methods used only in compareStrictly() ===== }