summaryrefslogtreecommitdiff
path: root/java/com/android/dialer/smartdial
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/android/dialer/smartdial')
-rw-r--r--java/com/android/dialer/smartdial/LatinSmartDialMap.java784
-rw-r--r--java/com/android/dialer/smartdial/SmartDialMap.java60
-rw-r--r--java/com/android/dialer/smartdial/SmartDialMatchPosition.java70
-rw-r--r--java/com/android/dialer/smartdial/SmartDialNameMatcher.java434
-rw-r--r--java/com/android/dialer/smartdial/SmartDialPrefix.java605
5 files changed, 1953 insertions, 0 deletions
diff --git a/java/com/android/dialer/smartdial/LatinSmartDialMap.java b/java/com/android/dialer/smartdial/LatinSmartDialMap.java
new file mode 100644
index 000000000..c512c5d4a
--- /dev/null
+++ b/java/com/android/dialer/smartdial/LatinSmartDialMap.java
@@ -0,0 +1,784 @@
+/*
+ * Copyright (C) 2016 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 com.android.dialer.smartdial;
+
+public class LatinSmartDialMap implements SmartDialMap {
+
+ private static final char[] LATIN_LETTERS_TO_DIGITS = {
+ '2',
+ '2',
+ '2', // A,B,C -> 2
+ '3',
+ '3',
+ '3', // D,E,F -> 3
+ '4',
+ '4',
+ '4', // G,H,I -> 4
+ '5',
+ '5',
+ '5', // J,K,L -> 5
+ '6',
+ '6',
+ '6', // M,N,O -> 6
+ '7',
+ '7',
+ '7',
+ '7', // P,Q,R,S -> 7
+ '8',
+ '8',
+ '8', // T,U,V -> 8
+ '9',
+ '9',
+ '9',
+ '9' // W,X,Y,Z -> 9
+ };
+
+ @Override
+ public boolean isValidDialpadAlphabeticChar(char ch) {
+ return (ch >= 'a' && ch <= 'z');
+ }
+
+ @Override
+ public boolean isValidDialpadNumericChar(char ch) {
+ return (ch >= '0' && ch <= '9');
+ }
+
+ @Override
+ public boolean isValidDialpadCharacter(char ch) {
+ return (isValidDialpadAlphabeticChar(ch) || isValidDialpadNumericChar(ch));
+ }
+
+ /*
+ * The switch statement in this function was generated using the python code:
+ * from unidecode import unidecode
+ * for i in range(192, 564):
+ * char = unichr(i)
+ * decoded = unidecode(char)
+ * # Unicode characters that decompose into multiple characters i.e.
+ * # into ss are not supported for now
+ * if (len(decoded) == 1 and decoded.isalpha()):
+ * print "case '" + char + "': return '" + unidecode(char) + "';"
+ *
+ * This gives us a way to map characters containing accents/diacritics to their
+ * alphabetic equivalents. The unidecode library can be found at:
+ * http://pypi.python.org/pypi/Unidecode/0.04.1
+ *
+ * Also remaps all upper case latin characters to their lower case equivalents.
+ */
+ @Override
+ public char normalizeCharacter(char ch) {
+ switch (ch) {
+ case 'À':
+ return 'a';
+ case 'Á':
+ return 'a';
+ case 'Â':
+ return 'a';
+ case 'Ã':
+ return 'a';
+ case 'Ä':
+ return 'a';
+ case 'Å':
+ return 'a';
+ case 'Ç':
+ return 'c';
+ case 'È':
+ return 'e';
+ case 'É':
+ return 'e';
+ case 'Ê':
+ return 'e';
+ case 'Ë':
+ return 'e';
+ case 'Ì':
+ return 'i';
+ case 'Í':
+ return 'i';
+ case 'Î':
+ return 'i';
+ case 'Ï':
+ return 'i';
+ case 'Ð':
+ return 'd';
+ case 'Ñ':
+ return 'n';
+ case 'Ò':
+ return 'o';
+ case 'Ó':
+ return 'o';
+ case 'Ô':
+ return 'o';
+ case 'Õ':
+ return 'o';
+ case 'Ö':
+ return 'o';
+ case '×':
+ return 'x';
+ case 'Ø':
+ return 'o';
+ case 'Ù':
+ return 'u';
+ case 'Ú':
+ return 'u';
+ case 'Û':
+ return 'u';
+ case 'Ü':
+ return 'u';
+ case 'Ý':
+ return 'u';
+ case 'à':
+ return 'a';
+ case 'á':
+ return 'a';
+ case 'â':
+ return 'a';
+ case 'ã':
+ return 'a';
+ case 'ä':
+ return 'a';
+ case 'å':
+ return 'a';
+ case 'ç':
+ return 'c';
+ case 'è':
+ return 'e';
+ case 'é':
+ return 'e';
+ case 'ê':
+ return 'e';
+ case 'ë':
+ return 'e';
+ case 'ì':
+ return 'i';
+ case 'í':
+ return 'i';
+ case 'î':
+ return 'i';
+ case 'ï':
+ return 'i';
+ case 'ð':
+ return 'd';
+ case 'ñ':
+ return 'n';
+ case 'ò':
+ return 'o';
+ case 'ó':
+ return 'o';
+ case 'ô':
+ return 'o';
+ case 'õ':
+ return 'o';
+ case 'ö':
+ return 'o';
+ case 'ø':
+ return 'o';
+ case 'ù':
+ return 'u';
+ case 'ú':
+ return 'u';
+ case 'û':
+ return 'u';
+ case 'ü':
+ return 'u';
+ case 'ý':
+ return 'y';
+ case 'ÿ':
+ return 'y';
+ case 'Ā':
+ return 'a';
+ case 'ā':
+ return 'a';
+ case 'Ă':
+ return 'a';
+ case 'ă':
+ return 'a';
+ case 'Ą':
+ return 'a';
+ case 'ą':
+ return 'a';
+ case 'Ć':
+ return 'c';
+ case 'ć':
+ return 'c';
+ case 'Ĉ':
+ return 'c';
+ case 'ĉ':
+ return 'c';
+ case 'Ċ':
+ return 'c';
+ case 'ċ':
+ return 'c';
+ case 'Č':
+ return 'c';
+ case 'č':
+ return 'c';
+ case 'Ď':
+ return 'd';
+ case 'ď':
+ return 'd';
+ case 'Đ':
+ return 'd';
+ case 'đ':
+ return 'd';
+ case 'Ē':
+ return 'e';
+ case 'ē':
+ return 'e';
+ case 'Ĕ':
+ return 'e';
+ case 'ĕ':
+ return 'e';
+ case 'Ė':
+ return 'e';
+ case 'ė':
+ return 'e';
+ case 'Ę':
+ return 'e';
+ case 'ę':
+ return 'e';
+ case 'Ě':
+ return 'e';
+ case 'ě':
+ return 'e';
+ case 'Ĝ':
+ return 'g';
+ case 'ĝ':
+ return 'g';
+ case 'Ğ':
+ return 'g';
+ case 'ğ':
+ return 'g';
+ case 'Ġ':
+ return 'g';
+ case 'ġ':
+ return 'g';
+ case 'Ģ':
+ return 'g';
+ case 'ģ':
+ return 'g';
+ case 'Ĥ':
+ return 'h';
+ case 'ĥ':
+ return 'h';
+ case 'Ħ':
+ return 'h';
+ case 'ħ':
+ return 'h';
+ case 'Ĩ':
+ return 'i';
+ case 'ĩ':
+ return 'i';
+ case 'Ī':
+ return 'i';
+ case 'ī':
+ return 'i';
+ case 'Ĭ':
+ return 'i';
+ case 'ĭ':
+ return 'i';
+ case 'Į':
+ return 'i';
+ case 'į':
+ return 'i';
+ case 'İ':
+ return 'i';
+ case 'ı':
+ return 'i';
+ case 'Ĵ':
+ return 'j';
+ case 'ĵ':
+ return 'j';
+ case 'Ķ':
+ return 'k';
+ case 'ķ':
+ return 'k';
+ case 'ĸ':
+ return 'k';
+ case 'Ĺ':
+ return 'l';
+ case 'ĺ':
+ return 'l';
+ case 'Ļ':
+ return 'l';
+ case 'ļ':
+ return 'l';
+ case 'Ľ':
+ return 'l';
+ case 'ľ':
+ return 'l';
+ case 'Ŀ':
+ return 'l';
+ case 'ŀ':
+ return 'l';
+ case 'Ł':
+ return 'l';
+ case 'ł':
+ return 'l';
+ case 'Ń':
+ return 'n';
+ case 'ń':
+ return 'n';
+ case 'Ņ':
+ return 'n';
+ case 'ņ':
+ return 'n';
+ case 'Ň':
+ return 'n';
+ case 'ň':
+ return 'n';
+ case 'Ō':
+ return 'o';
+ case 'ō':
+ return 'o';
+ case 'Ŏ':
+ return 'o';
+ case 'ŏ':
+ return 'o';
+ case 'Ő':
+ return 'o';
+ case 'ő':
+ return 'o';
+ case 'Ŕ':
+ return 'r';
+ case 'ŕ':
+ return 'r';
+ case 'Ŗ':
+ return 'r';
+ case 'ŗ':
+ return 'r';
+ case 'Ř':
+ return 'r';
+ case 'ř':
+ return 'r';
+ case 'Ś':
+ return 's';
+ case 'ś':
+ return 's';
+ case 'Ŝ':
+ return 's';
+ case 'ŝ':
+ return 's';
+ case 'Ş':
+ return 's';
+ case 'ş':
+ return 's';
+ case 'Š':
+ return 's';
+ case 'š':
+ return 's';
+ case 'Ţ':
+ return 't';
+ case 'ţ':
+ return 't';
+ case 'Ť':
+ return 't';
+ case 'ť':
+ return 't';
+ case 'Ŧ':
+ return 't';
+ case 'ŧ':
+ return 't';
+ case 'Ũ':
+ return 'u';
+ case 'ũ':
+ return 'u';
+ case 'Ū':
+ return 'u';
+ case 'ū':
+ return 'u';
+ case 'Ŭ':
+ return 'u';
+ case 'ŭ':
+ return 'u';
+ case 'Ů':
+ return 'u';
+ case 'ů':
+ return 'u';
+ case 'Ű':
+ return 'u';
+ case 'ű':
+ return 'u';
+ case 'Ų':
+ return 'u';
+ case 'ų':
+ return 'u';
+ case 'Ŵ':
+ return 'w';
+ case 'ŵ':
+ return 'w';
+ case 'Ŷ':
+ return 'y';
+ case 'ŷ':
+ return 'y';
+ case 'Ÿ':
+ return 'y';
+ case 'Ź':
+ return 'z';
+ case 'ź':
+ return 'z';
+ case 'Ż':
+ return 'z';
+ case 'ż':
+ return 'z';
+ case 'Ž':
+ return 'z';
+ case 'ž':
+ return 'z';
+ case 'ſ':
+ return 's';
+ case 'ƀ':
+ return 'b';
+ case 'Ɓ':
+ return 'b';
+ case 'Ƃ':
+ return 'b';
+ case 'ƃ':
+ return 'b';
+ case 'Ɔ':
+ return 'o';
+ case 'Ƈ':
+ return 'c';
+ case 'ƈ':
+ return 'c';
+ case 'Ɖ':
+ return 'd';
+ case 'Ɗ':
+ return 'd';
+ case 'Ƌ':
+ return 'd';
+ case 'ƌ':
+ return 'd';
+ case 'ƍ':
+ return 'd';
+ case 'Ɛ':
+ return 'e';
+ case 'Ƒ':
+ return 'f';
+ case 'ƒ':
+ return 'f';
+ case 'Ɠ':
+ return 'g';
+ case 'Ɣ':
+ return 'g';
+ case 'Ɩ':
+ return 'i';
+ case 'Ɨ':
+ return 'i';
+ case 'Ƙ':
+ return 'k';
+ case 'ƙ':
+ return 'k';
+ case 'ƚ':
+ return 'l';
+ case 'ƛ':
+ return 'l';
+ case 'Ɯ':
+ return 'w';
+ case 'Ɲ':
+ return 'n';
+ case 'ƞ':
+ return 'n';
+ case 'Ɵ':
+ return 'o';
+ case 'Ơ':
+ return 'o';
+ case 'ơ':
+ return 'o';
+ case 'Ƥ':
+ return 'p';
+ case 'ƥ':
+ return 'p';
+ case 'ƫ':
+ return 't';
+ case 'Ƭ':
+ return 't';
+ case 'ƭ':
+ return 't';
+ case 'Ʈ':
+ return 't';
+ case 'Ư':
+ return 'u';
+ case 'ư':
+ return 'u';
+ case 'Ʊ':
+ return 'y';
+ case 'Ʋ':
+ return 'v';
+ case 'Ƴ':
+ return 'y';
+ case 'ƴ':
+ return 'y';
+ case 'Ƶ':
+ return 'z';
+ case 'ƶ':
+ return 'z';
+ case 'ƿ':
+ return 'w';
+ case 'Ǎ':
+ return 'a';
+ case 'ǎ':
+ return 'a';
+ case 'Ǐ':
+ return 'i';
+ case 'ǐ':
+ return 'i';
+ case 'Ǒ':
+ return 'o';
+ case 'ǒ':
+ return 'o';
+ case 'Ǔ':
+ return 'u';
+ case 'ǔ':
+ return 'u';
+ case 'Ǖ':
+ return 'u';
+ case 'ǖ':
+ return 'u';
+ case 'Ǘ':
+ return 'u';
+ case 'ǘ':
+ return 'u';
+ case 'Ǚ':
+ return 'u';
+ case 'ǚ':
+ return 'u';
+ case 'Ǜ':
+ return 'u';
+ case 'ǜ':
+ return 'u';
+ case 'Ǟ':
+ return 'a';
+ case 'ǟ':
+ return 'a';
+ case 'Ǡ':
+ return 'a';
+ case 'ǡ':
+ return 'a';
+ case 'Ǥ':
+ return 'g';
+ case 'ǥ':
+ return 'g';
+ case 'Ǧ':
+ return 'g';
+ case 'ǧ':
+ return 'g';
+ case 'Ǩ':
+ return 'k';
+ case 'ǩ':
+ return 'k';
+ case 'Ǫ':
+ return 'o';
+ case 'ǫ':
+ return 'o';
+ case 'Ǭ':
+ return 'o';
+ case 'ǭ':
+ return 'o';
+ case 'ǰ':
+ return 'j';
+ case 'Dz':
+ return 'd';
+ case 'Ǵ':
+ return 'g';
+ case 'ǵ':
+ return 'g';
+ case 'Ƿ':
+ return 'w';
+ case 'Ǹ':
+ return 'n';
+ case 'ǹ':
+ return 'n';
+ case 'Ǻ':
+ return 'a';
+ case 'ǻ':
+ return 'a';
+ case 'Ǿ':
+ return 'o';
+ case 'ǿ':
+ return 'o';
+ case 'Ȁ':
+ return 'a';
+ case 'ȁ':
+ return 'a';
+ case 'Ȃ':
+ return 'a';
+ case 'ȃ':
+ return 'a';
+ case 'Ȅ':
+ return 'e';
+ case 'ȅ':
+ return 'e';
+ case 'Ȇ':
+ return 'e';
+ case 'ȇ':
+ return 'e';
+ case 'Ȉ':
+ return 'i';
+ case 'ȉ':
+ return 'i';
+ case 'Ȋ':
+ return 'i';
+ case 'ȋ':
+ return 'i';
+ case 'Ȍ':
+ return 'o';
+ case 'ȍ':
+ return 'o';
+ case 'Ȏ':
+ return 'o';
+ case 'ȏ':
+ return 'o';
+ case 'Ȑ':
+ return 'r';
+ case 'ȑ':
+ return 'r';
+ case 'Ȓ':
+ return 'r';
+ case 'ȓ':
+ return 'r';
+ case 'Ȕ':
+ return 'u';
+ case 'ȕ':
+ return 'u';
+ case 'Ȗ':
+ return 'u';
+ case 'ȗ':
+ return 'u';
+ case 'Ș':
+ return 's';
+ case 'ș':
+ return 's';
+ case 'Ț':
+ return 't';
+ case 'ț':
+ return 't';
+ case 'Ȝ':
+ return 'y';
+ case 'ȝ':
+ return 'y';
+ case 'Ȟ':
+ return 'h';
+ case 'ȟ':
+ return 'h';
+ case 'Ȥ':
+ return 'z';
+ case 'ȥ':
+ return 'z';
+ case 'Ȧ':
+ return 'a';
+ case 'ȧ':
+ return 'a';
+ case 'Ȩ':
+ return 'e';
+ case 'ȩ':
+ return 'e';
+ case 'Ȫ':
+ return 'o';
+ case 'ȫ':
+ return 'o';
+ case 'Ȭ':
+ return 'o';
+ case 'ȭ':
+ return 'o';
+ case 'Ȯ':
+ return 'o';
+ case 'ȯ':
+ return 'o';
+ case 'Ȱ':
+ return 'o';
+ case 'ȱ':
+ return 'o';
+ case 'Ȳ':
+ return 'y';
+ case 'ȳ':
+ return 'y';
+ case 'A':
+ return 'a';
+ case 'B':
+ return 'b';
+ case 'C':
+ return 'c';
+ case 'D':
+ return 'd';
+ case 'E':
+ return 'e';
+ case 'F':
+ return 'f';
+ case 'G':
+ return 'g';
+ case 'H':
+ return 'h';
+ case 'I':
+ return 'i';
+ case 'J':
+ return 'j';
+ case 'K':
+ return 'k';
+ case 'L':
+ return 'l';
+ case 'M':
+ return 'm';
+ case 'N':
+ return 'n';
+ case 'O':
+ return 'o';
+ case 'P':
+ return 'p';
+ case 'Q':
+ return 'q';
+ case 'R':
+ return 'r';
+ case 'S':
+ return 's';
+ case 'T':
+ return 't';
+ case 'U':
+ return 'u';
+ case 'V':
+ return 'v';
+ case 'W':
+ return 'w';
+ case 'X':
+ return 'x';
+ case 'Y':
+ return 'y';
+ case 'Z':
+ return 'z';
+ default:
+ return ch;
+ }
+ }
+
+ @Override
+ public byte getDialpadIndex(char ch) {
+ if (ch >= '0' && ch <= '9') {
+ return (byte) (ch - '0');
+ } else if (ch >= 'a' && ch <= 'z') {
+ return (byte) (LATIN_LETTERS_TO_DIGITS[ch - 'a'] - '0');
+ } else {
+ return -1;
+ }
+ }
+
+ @Override
+ public char getDialpadNumericCharacter(char ch) {
+ if (ch >= 'a' && ch <= 'z') {
+ return LATIN_LETTERS_TO_DIGITS[ch - 'a'];
+ }
+ return ch;
+ }
+}
diff --git a/java/com/android/dialer/smartdial/SmartDialMap.java b/java/com/android/dialer/smartdial/SmartDialMap.java
new file mode 100644
index 000000000..9638929a6
--- /dev/null
+++ b/java/com/android/dialer/smartdial/SmartDialMap.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2016 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 com.android.dialer.smartdial;
+
+/**
+ * Note: These methods currently take characters as arguments. For future planned language support,
+ * they will need to be changed to use codepoints instead of characters.
+ *
+ * <p>http://docs.oracle.com/javase/6/docs/api/java/lang/String.html#codePointAt(int)
+ *
+ * <p>If/when this change is made, LatinSmartDialMap(which operates on chars) will continue to work
+ * by simply casting from a codepoint to a character.
+ */
+public interface SmartDialMap {
+
+ /*
+ * Returns true if the provided character can be mapped to a key on the dialpad
+ */
+ boolean isValidDialpadCharacter(char ch);
+
+ /*
+ * Returns true if the provided character is a letter, and can be mapped to a key on the dialpad
+ */
+ boolean isValidDialpadAlphabeticChar(char ch);
+
+ /*
+ * Returns true if the provided character is a digit, and can be mapped to a key on the dialpad
+ */
+ boolean isValidDialpadNumericChar(char ch);
+
+ /*
+ * Get the index of the key on the dialpad which the character corresponds to
+ */
+ byte getDialpadIndex(char ch);
+
+ /*
+ * Get the actual numeric character on the dialpad which the character corresponds to
+ */
+ char getDialpadNumericCharacter(char ch);
+
+ /*
+ * Converts uppercase characters to lower case ones, and on a best effort basis, strips accents
+ * from accented characters.
+ */
+ char normalizeCharacter(char ch);
+}
diff --git a/java/com/android/dialer/smartdial/SmartDialMatchPosition.java b/java/com/android/dialer/smartdial/SmartDialMatchPosition.java
new file mode 100644
index 000000000..8056ad723
--- /dev/null
+++ b/java/com/android/dialer/smartdial/SmartDialMatchPosition.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2012 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 com.android.dialer.smartdial;
+
+import android.util.Log;
+import java.util.ArrayList;
+
+/**
+ * Stores information about a range of characters matched in a display name The integers start and
+ * end indicate that the range start to end (exclusive) correspond to some characters in the query.
+ * Used to highlight certain parts of the contact's display name to indicate that those ranges
+ * matched the user's query.
+ */
+public class SmartDialMatchPosition {
+
+ private static final String TAG = SmartDialMatchPosition.class.getSimpleName();
+
+ public int start;
+ public int end;
+
+ public SmartDialMatchPosition(int start, int end) {
+ this.start = start;
+ this.end = end;
+ }
+
+ /**
+ * Used by {@link SmartDialNameMatcher} to advance the positions of a match position found in a
+ * sub query.
+ *
+ * @param inList ArrayList of SmartDialMatchPositions to modify.
+ * @param toAdvance Offset to modify by.
+ */
+ public static void advanceMatchPositions(
+ ArrayList<SmartDialMatchPosition> inList, int toAdvance) {
+ for (int i = 0; i < inList.size(); i++) {
+ inList.get(i).advance(toAdvance);
+ }
+ }
+
+ /**
+ * Used mainly for debug purposes. Displays contents of an ArrayList of SmartDialMatchPositions.
+ *
+ * @param list ArrayList of SmartDialMatchPositions to print out in a human readable fashion.
+ */
+ public static void print(ArrayList<SmartDialMatchPosition> list) {
+ for (int i = 0; i < list.size(); i++) {
+ SmartDialMatchPosition m = list.get(i);
+ Log.d(TAG, "[" + m.start + "," + m.end + "]");
+ }
+ }
+
+ private void advance(int toAdvance) {
+ this.start += toAdvance;
+ this.end += toAdvance;
+ }
+}
diff --git a/java/com/android/dialer/smartdial/SmartDialNameMatcher.java b/java/com/android/dialer/smartdial/SmartDialNameMatcher.java
new file mode 100644
index 000000000..a1580a0ce
--- /dev/null
+++ b/java/com/android/dialer/smartdial/SmartDialNameMatcher.java
@@ -0,0 +1,434 @@
+/*
+ * Copyright (C) 2012 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 com.android.dialer.smartdial;
+
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import com.android.dialer.smartdial.SmartDialPrefix.PhoneNumberTokens;
+import java.util.ArrayList;
+
+/**
+ * {@link #SmartDialNameMatcher} contains utility functions to remove accents from accented
+ * characters and normalize a phone number. It also contains the matching logic that determines if a
+ * contact's display name matches a numeric query. The boolean variable {@link #ALLOW_INITIAL_MATCH}
+ * controls the behavior of the matching logic and determines whether we allow matches like 57 -
+ * (J)ohn (S)mith.
+ */
+public class SmartDialNameMatcher {
+
+ public static final SmartDialMap LATIN_SMART_DIAL_MAP = new LatinSmartDialMap();
+ // Whether or not we allow matches like 57 - (J)ohn (S)mith
+ private static final boolean ALLOW_INITIAL_MATCH = true;
+
+ // The maximum length of the initial we will match - typically set to 1 to minimize false
+ // positives
+ private static final int INITIAL_LENGTH_LIMIT = 1;
+
+ private final ArrayList<SmartDialMatchPosition> mMatchPositions = new ArrayList<>();
+ private final SmartDialMap mMap;
+ private String mQuery;
+ private String mNameMatchMask = "";
+ private String mPhoneNumberMatchMask = "";
+
+ // Controls whether to treat an empty query as a match (with anything).
+ private boolean mShouldMatchEmptyQuery = false;
+
+ @VisibleForTesting
+ public SmartDialNameMatcher(String query) {
+ this(query, LATIN_SMART_DIAL_MAP);
+ }
+
+ public SmartDialNameMatcher(String query, SmartDialMap map) {
+ mQuery = query;
+ mMap = map;
+ }
+
+ /**
+ * Strips a phone number of unnecessary characters (spaces, dashes, etc.)
+ *
+ * @param number Phone number we want to normalize
+ * @return Phone number consisting of digits from 0-9
+ */
+ public static String normalizeNumber(String number, SmartDialMap map) {
+ return normalizeNumber(number, 0, map);
+ }
+
+ /**
+ * Strips a phone number of unnecessary characters (spaces, dashes, etc.)
+ *
+ * @param number Phone number we want to normalize
+ * @param offset Offset to start from
+ * @return Phone number consisting of digits from 0-9
+ */
+ public static String normalizeNumber(String number, int offset, SmartDialMap map) {
+ final StringBuilder s = new StringBuilder();
+ for (int i = offset; i < number.length(); i++) {
+ char ch = number.charAt(i);
+ if (map.isValidDialpadNumericChar(ch)) {
+ s.append(ch);
+ }
+ }
+ return s.toString();
+ }
+
+ /**
+ * Constructs empty highlight mask. Bit 0 at a position means there is no match, Bit 1 means there
+ * is a match and should be highlighted in the TextView.
+ *
+ * @param builder StringBuilder object
+ * @param length Length of the desired mask.
+ */
+ private void constructEmptyMask(StringBuilder builder, int length) {
+ for (int i = 0; i < length; ++i) {
+ builder.append("0");
+ }
+ }
+
+ /**
+ * Replaces the 0-bit at a position with 1-bit, indicating that there is a match.
+ *
+ * @param builder StringBuilder object.
+ * @param matchPos Match Positions to mask as 1.
+ */
+ private void replaceBitInMask(StringBuilder builder, SmartDialMatchPosition matchPos) {
+ for (int i = matchPos.start; i < matchPos.end; ++i) {
+ builder.replace(i, i + 1, "1");
+ }
+ }
+
+ /**
+ * Matches a phone number against a query. Let the test application overwrite the NANP setting.
+ *
+ * @param phoneNumber - Raw phone number
+ * @param query - Normalized query (only contains numbers from 0-9)
+ * @param useNanp - Overwriting nanp setting boolean, used for testing.
+ * @return {@literal null} if the number and the query don't match, a valid SmartDialMatchPosition
+ * with the matching positions otherwise
+ */
+ @VisibleForTesting
+ @Nullable
+ public SmartDialMatchPosition matchesNumber(String phoneNumber, String query, boolean useNanp) {
+ if (TextUtils.isEmpty(phoneNumber)) {
+ return mShouldMatchEmptyQuery ? new SmartDialMatchPosition(0, 0) : null;
+ }
+ StringBuilder builder = new StringBuilder();
+ constructEmptyMask(builder, phoneNumber.length());
+ mPhoneNumberMatchMask = builder.toString();
+
+ // Try matching the number as is
+ SmartDialMatchPosition matchPos = matchesNumberWithOffset(phoneNumber, query, 0);
+ if (matchPos == null) {
+ final PhoneNumberTokens phoneNumberTokens = SmartDialPrefix.parsePhoneNumber(phoneNumber);
+
+ if (phoneNumberTokens == null) {
+ return matchPos;
+ }
+ if (phoneNumberTokens.countryCodeOffset != 0) {
+ matchPos = matchesNumberWithOffset(phoneNumber, query, phoneNumberTokens.countryCodeOffset);
+ }
+ if (matchPos == null && phoneNumberTokens.nanpCodeOffset != 0 && useNanp) {
+ matchPos = matchesNumberWithOffset(phoneNumber, query, phoneNumberTokens.nanpCodeOffset);
+ }
+ }
+ if (matchPos != null) {
+ replaceBitInMask(builder, matchPos);
+ mPhoneNumberMatchMask = builder.toString();
+ }
+ return matchPos;
+ }
+
+ /**
+ * Matches a phone number against the saved query, taking care of formatting characters and also
+ * taking into account country code prefixes and special NANP number treatment.
+ *
+ * @param phoneNumber - Raw phone number
+ * @return {@literal null} if the number and the query don't match, a valid SmartDialMatchPosition
+ * with the matching positions otherwise
+ */
+ public SmartDialMatchPosition matchesNumber(String phoneNumber) {
+ return matchesNumber(phoneNumber, mQuery, true);
+ }
+
+ /**
+ * Matches a phone number against a query, taking care of formatting characters and also taking
+ * into account country code prefixes and special NANP number treatment.
+ *
+ * @param phoneNumber - Raw phone number
+ * @param query - Normalized query (only contains numbers from 0-9)
+ * @return {@literal null} if the number and the query don't match, a valid SmartDialMatchPosition
+ * with the matching positions otherwise
+ */
+ public SmartDialMatchPosition matchesNumber(String phoneNumber, String query) {
+ return matchesNumber(phoneNumber, query, true);
+ }
+
+ /**
+ * Matches a phone number against a query, taking care of formatting characters
+ *
+ * @param phoneNumber - Raw phone number
+ * @param query - Normalized query (only contains numbers from 0-9)
+ * @param offset - The position in the number to start the match against (used to ignore leading
+ * prefixes/country codes)
+ * @return {@literal null} if the number and the query don't match, a valid SmartDialMatchPosition
+ * with the matching positions otherwise
+ */
+ private SmartDialMatchPosition matchesNumberWithOffset(
+ String phoneNumber, String query, int offset) {
+ if (TextUtils.isEmpty(phoneNumber) || TextUtils.isEmpty(query)) {
+ return mShouldMatchEmptyQuery ? new SmartDialMatchPosition(offset, offset) : null;
+ }
+ int queryAt = 0;
+ int numberAt = offset;
+ for (int i = offset; i < phoneNumber.length(); i++) {
+ if (queryAt == query.length()) {
+ break;
+ }
+ char ch = phoneNumber.charAt(i);
+ if (mMap.isValidDialpadNumericChar(ch)) {
+ if (ch != query.charAt(queryAt)) {
+ return null;
+ }
+ queryAt++;
+ } else {
+ if (queryAt == 0) {
+ // Found a separator before any part of the query was matched, so advance the
+ // offset to avoid prematurely highlighting separators before the rest of the
+ // query.
+ // E.g. don't highlight the first '-' if we're matching 1-510-111-1111 with
+ // '510'.
+ // However, if the current offset is 0, just include the beginning separators
+ // anyway, otherwise the highlighting ends up looking weird.
+ // E.g. if we're matching (510)-111-1111 with '510', we should include the
+ // first '('.
+ if (offset != 0) {
+ offset++;
+ }
+ }
+ }
+ numberAt++;
+ }
+ return new SmartDialMatchPosition(0 + offset, numberAt);
+ }
+
+ /**
+ * This function iterates through each token in the display name, trying to match the query to the
+ * numeric equivalent of the token.
+ *
+ * <p>A token is defined as a range in the display name delimited by characters that have no latin
+ * alphabet equivalents (e.g. spaces - ' ', periods - ',', underscores - '_' or chinese characters
+ * - '王'). Transliteration from non-latin characters to latin character will be done on a best
+ * effort basis - e.g. 'Ü' - 'u'.
+ *
+ * <p>For example, the display name "Phillips Thomas Jr" contains three tokens: "phillips",
+ * "thomas", and "jr".
+ *
+ * <p>A match must begin at the start of a token. For example, typing 846(Tho) would match
+ * "Phillips Thomas", but 466(hom) would not.
+ *
+ * <p>Also, a match can extend across tokens. For example, typing 37337(FredS) would match (Fred
+ * S)mith.
+ *
+ * @param displayName The normalized(no accented characters) display name we intend to match
+ * against.
+ * @param query The string of digits that we want to match the display name to.
+ * @param matchList An array list of {@link SmartDialMatchPosition}s that we add matched positions
+ * to.
+ * @return Returns true if a combination of the tokens in displayName match the query string
+ * contained in query. If the function returns true, matchList will contain an ArrayList of
+ * match positions (multiple matches correspond to initial matches).
+ */
+ @VisibleForTesting
+ boolean matchesCombination(
+ String displayName, String query, ArrayList<SmartDialMatchPosition> matchList) {
+ StringBuilder builder = new StringBuilder();
+ constructEmptyMask(builder, displayName.length());
+ mNameMatchMask = builder.toString();
+ final int nameLength = displayName.length();
+ final int queryLength = query.length();
+
+ if (nameLength < queryLength) {
+ return false;
+ }
+
+ if (queryLength == 0) {
+ return false;
+ }
+
+ // The current character index in displayName
+ // E.g. 3 corresponds to 'd' in "Fred Smith"
+ int nameStart = 0;
+
+ // The current character in the query we are trying to match the displayName against
+ int queryStart = 0;
+
+ // The start position of the current token we are inspecting
+ int tokenStart = 0;
+
+ // The number of non-alphabetic characters we've encountered so far in the current match.
+ // E.g. if we've currently matched 3733764849 to (Fred Smith W)illiam, then the
+ // seperatorCount should be 2. This allows us to correctly calculate offsets for the match
+ // positions
+ int seperatorCount = 0;
+
+ ArrayList<SmartDialMatchPosition> partial = new ArrayList<SmartDialMatchPosition>();
+ // Keep going until we reach the end of displayName
+ while (nameStart < nameLength && queryStart < queryLength) {
+ char ch = displayName.charAt(nameStart);
+ // Strip diacritics from accented characters if any
+ ch = mMap.normalizeCharacter(ch);
+ if (mMap.isValidDialpadCharacter(ch)) {
+ if (mMap.isValidDialpadAlphabeticChar(ch)) {
+ ch = mMap.getDialpadNumericCharacter(ch);
+ }
+ if (ch != query.charAt(queryStart)) {
+ // Failed to match the current character in the query.
+
+ // Case 1: Failed to match the first character in the query. Skip to the next
+ // token since there is no chance of this token matching the query.
+
+ // Case 2: Previous characters in the query matched, but the current character
+ // failed to match. This happened in the middle of a token. Skip to the next
+ // token since there is no chance of this token matching the query.
+
+ // Case 3: Previous characters in the query matched, but the current character
+ // failed to match. This happened right at the start of the current token. In
+ // this case, we should restart the query and try again with the current token.
+ // Otherwise, we would fail to match a query like "964"(yog) against a name
+ // Yo-Yoghurt because the query match would fail on the 3rd character, and
+ // then skip to the end of the "Yoghurt" token.
+
+ if (queryStart == 0
+ || mMap.isValidDialpadCharacter(
+ mMap.normalizeCharacter(displayName.charAt(nameStart - 1)))) {
+ // skip to the next token, in the case of 1 or 2.
+ while (nameStart < nameLength
+ && mMap.isValidDialpadCharacter(
+ mMap.normalizeCharacter(displayName.charAt(nameStart)))) {
+ nameStart++;
+ }
+ nameStart++;
+ }
+
+ // Restart the query and set the correct token position
+ queryStart = 0;
+ seperatorCount = 0;
+ tokenStart = nameStart;
+ } else {
+ if (queryStart == queryLength - 1) {
+
+ // As much as possible, we prioritize a full token match over a sub token
+ // one so if we find a full token match, we can return right away
+ matchList.add(
+ new SmartDialMatchPosition(tokenStart, queryLength + tokenStart + seperatorCount));
+ for (SmartDialMatchPosition match : matchList) {
+ replaceBitInMask(builder, match);
+ }
+ mNameMatchMask = builder.toString();
+ return true;
+ } else if (ALLOW_INITIAL_MATCH && queryStart < INITIAL_LENGTH_LIMIT) {
+ // we matched the first character.
+ // branch off and see if we can find another match with the remaining
+ // characters in the query string and the remaining tokens
+ // find the next separator in the query string
+ int j;
+ for (j = nameStart; j < nameLength; j++) {
+ if (!mMap.isValidDialpadCharacter(mMap.normalizeCharacter(displayName.charAt(j)))) {
+ break;
+ }
+ }
+ // this means there is at least one character left after the separator
+ if (j < nameLength - 1) {
+ final String remainder = displayName.substring(j + 1);
+ final ArrayList<SmartDialMatchPosition> partialTemp = new ArrayList<>();
+ if (matchesCombination(remainder, query.substring(queryStart + 1), partialTemp)) {
+
+ // store the list of possible match positions
+ SmartDialMatchPosition.advanceMatchPositions(partialTemp, j + 1);
+ partialTemp.add(0, new SmartDialMatchPosition(nameStart, nameStart + 1));
+ // we found a partial token match, store the data in a
+ // temp buffer and return it if we end up not finding a full
+ // token match
+ partial = partialTemp;
+ }
+ }
+ }
+ nameStart++;
+ queryStart++;
+ // we matched the current character in the name against one in the query,
+ // continue and see if the rest of the characters match
+ }
+ } else {
+ // found a separator, we skip this character and continue to the next one
+ nameStart++;
+ if (queryStart == 0) {
+ // This means we found a separator before the start of a token,
+ // so we should increment the token's start position to reflect its true
+ // start position
+ tokenStart = nameStart;
+ } else {
+ // Otherwise this separator was found in the middle of a token being matched,
+ // so increase the separator count
+ seperatorCount++;
+ }
+ }
+ }
+ // if we have no complete match at this point, then we attempt to fall back to the partial
+ // token match(if any). If we don't allow initial matching (ALLOW_INITIAL_MATCH = false)
+ // then partial will always be empty.
+ if (!partial.isEmpty()) {
+ matchList.addAll(partial);
+ for (SmartDialMatchPosition match : matchList) {
+ replaceBitInMask(builder, match);
+ }
+ mNameMatchMask = builder.toString();
+ return true;
+ }
+ return false;
+ }
+
+ public boolean matches(String displayName) {
+ mMatchPositions.clear();
+ return matchesCombination(displayName, mQuery, mMatchPositions);
+ }
+
+ public ArrayList<SmartDialMatchPosition> getMatchPositions() {
+ // Return a clone of mMatchPositions so that the caller can use it without
+ // worrying about it changing
+ return new ArrayList<SmartDialMatchPosition>(mMatchPositions);
+ }
+
+ public String getNameMatchPositionsInString() {
+ return mNameMatchMask;
+ }
+
+ public String getNumberMatchPositionsInString() {
+ return mPhoneNumberMatchMask;
+ }
+
+ public String getQuery() {
+ return mQuery;
+ }
+
+ public void setQuery(String query) {
+ mQuery = query;
+ }
+
+ public void setShouldMatchEmptyQuery(boolean matches) {
+ mShouldMatchEmptyQuery = matches;
+ }
+}
diff --git a/java/com/android/dialer/smartdial/SmartDialPrefix.java b/java/com/android/dialer/smartdial/SmartDialPrefix.java
new file mode 100644
index 000000000..a000e21c5
--- /dev/null
+++ b/java/com/android/dialer/smartdial/SmartDialPrefix.java
@@ -0,0 +1,605 @@
+/*
+ * Copyright (C) 2013 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 com.android.dialer.smartdial;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.support.annotation.VisibleForTesting;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Smart Dial utility class to find prefixes of contacts. It contains both methods to find supported
+ * prefix combinations for contact names, and also methods to find supported prefix combinations for
+ * contacts' phone numbers. Each contact name is separated into several tokens, such as first name,
+ * middle name, family name etc. Each phone number is also separated into country code, NANP area
+ * code, and local number if such separation is possible.
+ */
+public class SmartDialPrefix {
+
+ /**
+ * The number of starting and ending tokens in a contact's name considered for initials. For
+ * example, if both constants are set to 2, and a contact's name is "Albert Ben Charles Daniel Ed
+ * Foster", the first two tokens "Albert" "Ben", and last two tokens "Ed" "Foster" can be replaced
+ * by their initials in contact name matching. Users can look up this contact by combinations of
+ * his initials such as "AF" "BF" "EF" "ABF" "BEF" "ABEF" etc, but can not use combinations such
+ * as "CF" "DF" "ACF" "ADF" etc.
+ */
+ private static final int LAST_TOKENS_FOR_INITIALS = 2;
+
+ private static final int FIRST_TOKENS_FOR_INITIALS = 2;
+
+ /** The country code of the user's sim card obtained by calling getSimCountryIso */
+ private static final String PREF_USER_SIM_COUNTRY_CODE =
+ "DialtactsActivity_user_sim_country_code";
+
+ private static final String PREF_USER_SIM_COUNTRY_CODE_DEFAULT = null;
+ /** Dialpad mapping. */
+ private static final SmartDialMap mMap = new LatinSmartDialMap();
+
+ private static String sUserSimCountryCode = PREF_USER_SIM_COUNTRY_CODE_DEFAULT;
+ /** Indicates whether user is in NANP regions. */
+ private static boolean sUserInNanpRegion = false;
+ /** Set of country names that use NANP code. */
+ private static Set<String> sNanpCountries = null;
+ /** Set of supported country codes in front of the phone number. */
+ private static Set<String> sCountryCodes = null;
+
+ private static boolean sNanpInitialized = false;
+
+ /** Initializes the Nanp settings, and finds out whether user is in a NANP region. */
+ public static void initializeNanpSettings(Context context) {
+ final TelephonyManager manager =
+ (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ if (manager != null) {
+ sUserSimCountryCode = manager.getSimCountryIso();
+ }
+
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+
+ if (sUserSimCountryCode != null) {
+ /** Updates shared preferences with the latest country obtained from getSimCountryIso. */
+ prefs.edit().putString(PREF_USER_SIM_COUNTRY_CODE, sUserSimCountryCode).apply();
+ } else {
+ /** Uses previously stored country code if loading fails. */
+ sUserSimCountryCode =
+ prefs.getString(PREF_USER_SIM_COUNTRY_CODE, PREF_USER_SIM_COUNTRY_CODE_DEFAULT);
+ }
+ /** Queries the NANP country list to find out whether user is in a NANP region. */
+ sUserInNanpRegion = isCountryNanp(sUserSimCountryCode);
+ sNanpInitialized = true;
+ }
+
+ /**
+ * Parses a contact's name into a list of separated tokens.
+ *
+ * @param contactName Contact's name stored in string.
+ * @return A list of name tokens, for example separated first names, last name, etc.
+ */
+ public static ArrayList<String> parseToIndexTokens(String contactName) {
+ final int length = contactName.length();
+ final ArrayList<String> result = new ArrayList<>();
+ char c;
+ final StringBuilder currentIndexToken = new StringBuilder();
+ /**
+ * Iterates through the whole name string. If the current character is a valid character, append
+ * it to the current token. If the current character is not a valid character, for example space
+ * " ", mark the current token as complete and add it to the list of tokens.
+ */
+ for (int i = 0; i < length; i++) {
+ c = mMap.normalizeCharacter(contactName.charAt(i));
+ if (mMap.isValidDialpadCharacter(c)) {
+ /** Converts a character into the number on dialpad that represents the character. */
+ currentIndexToken.append(mMap.getDialpadIndex(c));
+ } else {
+ if (currentIndexToken.length() != 0) {
+ result.add(currentIndexToken.toString());
+ }
+ currentIndexToken.delete(0, currentIndexToken.length());
+ }
+ }
+
+ /** Adds the last token in case it has not been added. */
+ if (currentIndexToken.length() != 0) {
+ result.add(currentIndexToken.toString());
+ }
+ return result;
+ }
+
+ /**
+ * Generates a list of strings that any prefix of any string in the list can be used to look up
+ * the contact's name.
+ *
+ * @param index The contact's name in string.
+ * @return A List of strings, whose prefix can be used to look up the contact.
+ */
+ public static ArrayList<String> generateNamePrefixes(String index) {
+ final ArrayList<String> result = new ArrayList<>();
+
+ /** Parses the name into a list of tokens. */
+ final ArrayList<String> indexTokens = parseToIndexTokens(index);
+
+ if (indexTokens.size() > 0) {
+ /**
+ * Adds the full token combinations to the list. For example, a contact with name "Albert Ben
+ * Ed Foster" can be looked up by any prefix of the following strings "Foster" "EdFoster"
+ * "BenEdFoster" and "AlbertBenEdFoster". This covers all cases of look up that contains only
+ * one token, and that spans multiple continuous tokens.
+ */
+ final StringBuilder fullNameToken = new StringBuilder();
+ for (int i = indexTokens.size() - 1; i >= 0; i--) {
+ fullNameToken.insert(0, indexTokens.get(i));
+ result.add(fullNameToken.toString());
+ }
+
+ /**
+ * Adds initial combinations to the list, with the number of initials restricted by {@link
+ * #LAST_TOKENS_FOR_INITIALS} and {@link #FIRST_TOKENS_FOR_INITIALS}. For example, a contact
+ * with name "Albert Ben Ed Foster" can be looked up by any prefix of the following strings
+ * "EFoster" "BFoster" "BEFoster" "AFoster" "ABFoster" "AEFoster" and "ABEFoster". This covers
+ * all cases of initial lookup.
+ */
+ ArrayList<String> fullNames = new ArrayList<>();
+ fullNames.add(indexTokens.get(indexTokens.size() - 1));
+ final int recursiveNameStart = result.size();
+ int recursiveNameEnd = result.size();
+ String initial = "";
+ for (int i = indexTokens.size() - 2; i >= 0; i--) {
+ if ((i >= indexTokens.size() - LAST_TOKENS_FOR_INITIALS)
+ || (i < FIRST_TOKENS_FOR_INITIALS)) {
+ initial = indexTokens.get(i).substring(0, 1);
+
+ /** Recursively adds initial combinations to the list. */
+ for (int j = 0; j < fullNames.size(); ++j) {
+ result.add(initial + fullNames.get(j));
+ }
+ for (int j = recursiveNameStart; j < recursiveNameEnd; ++j) {
+ result.add(initial + result.get(j));
+ }
+ recursiveNameEnd = result.size();
+ final String currentFullName = fullNames.get(fullNames.size() - 1);
+ fullNames.add(indexTokens.get(i) + currentFullName);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Computes a list of number strings based on tokens of a given phone number. Any prefix of any
+ * string in the list can be used to look up the phone number. The list include the full phone
+ * number, the national number if there is a country code in the phone number, and the local
+ * number if there is an area code in the phone number following the NANP format. For example, if
+ * a user has phone number +41 71 394 8392, the list will contain 41713948392 and 713948392. Any
+ * prefix to either of the strings can be used to look up the phone number. If a user has a phone
+ * number +1 555-302-3029 (NANP format), the list will contain 15553023029, 5553023029, and
+ * 3023029.
+ *
+ * @param number String of user's phone number.
+ * @return A list of strings where any prefix of any entry can be used to look up the number.
+ */
+ public static ArrayList<String> parseToNumberTokens(String number) {
+ final ArrayList<String> result = new ArrayList<>();
+ if (!TextUtils.isEmpty(number)) {
+ /** Adds the full number to the list. */
+ result.add(SmartDialNameMatcher.normalizeNumber(number, mMap));
+
+ final PhoneNumberTokens phoneNumberTokens = parsePhoneNumber(number);
+ if (phoneNumberTokens == null) {
+ return result;
+ }
+
+ if (phoneNumberTokens.countryCodeOffset != 0) {
+ result.add(
+ SmartDialNameMatcher.normalizeNumber(
+ number, phoneNumberTokens.countryCodeOffset, mMap));
+ }
+
+ if (phoneNumberTokens.nanpCodeOffset != 0) {
+ result.add(
+ SmartDialNameMatcher.normalizeNumber(number, phoneNumberTokens.nanpCodeOffset, mMap));
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Parses a phone number to find out whether it has country code and NANP area code.
+ *
+ * @param number Raw phone number.
+ * @return a PhoneNumberToken instance with country code, NANP code information.
+ */
+ public static PhoneNumberTokens parsePhoneNumber(String number) {
+ String countryCode = "";
+ int countryCodeOffset = 0;
+ int nanpNumberOffset = 0;
+
+ if (!TextUtils.isEmpty(number)) {
+ String normalizedNumber = SmartDialNameMatcher.normalizeNumber(number, mMap);
+ if (number.charAt(0) == '+') {
+ /** If the number starts with '+', tries to find valid country code. */
+ for (int i = 1; i <= 1 + 3; i++) {
+ if (number.length() <= i) {
+ break;
+ }
+ countryCode = number.substring(1, i);
+ if (isValidCountryCode(countryCode)) {
+ countryCodeOffset = i;
+ break;
+ }
+ }
+ } else {
+ /**
+ * If the number does not start with '+', finds out whether it is in NANP format and has '1'
+ * preceding the number.
+ */
+ if ((normalizedNumber.length() == 11)
+ && (normalizedNumber.charAt(0) == '1')
+ && (sUserInNanpRegion)) {
+ countryCode = "1";
+ countryCodeOffset = number.indexOf(normalizedNumber.charAt(1));
+ if (countryCodeOffset == -1) {
+ countryCodeOffset = 0;
+ }
+ }
+ }
+
+ /** If user is in NANP region, finds out whether a number is in NANP format. */
+ if (sUserInNanpRegion) {
+ String areaCode = "";
+ if (countryCode.equals("") && normalizedNumber.length() == 10) {
+ /**
+ * if the number has no country code but fits the NANP format, extracts the NANP area
+ * code, and finds out offset of the local number.
+ */
+ areaCode = normalizedNumber.substring(0, 3);
+ } else if (countryCode.equals("1") && normalizedNumber.length() == 11) {
+ /**
+ * If the number has country code '1', finds out area code and offset of the local number.
+ */
+ areaCode = normalizedNumber.substring(1, 4);
+ }
+ if (!areaCode.equals("")) {
+ final int areaCodeIndex = number.indexOf(areaCode);
+ if (areaCodeIndex != -1) {
+ nanpNumberOffset = number.indexOf(areaCode) + 3;
+ }
+ }
+ }
+ }
+ return new PhoneNumberTokens(countryCode, countryCodeOffset, nanpNumberOffset);
+ }
+
+ /** Checkes whether a country code is valid. */
+ private static boolean isValidCountryCode(String countryCode) {
+ if (sCountryCodes == null) {
+ sCountryCodes = initCountryCodes();
+ }
+ return sCountryCodes.contains(countryCode);
+ }
+
+ private static Set<String> initCountryCodes() {
+ final HashSet<String> result = new HashSet<String>();
+ result.add("1");
+ result.add("7");
+ result.add("20");
+ result.add("27");
+ result.add("30");
+ result.add("31");
+ result.add("32");
+ result.add("33");
+ result.add("34");
+ result.add("36");
+ result.add("39");
+ result.add("40");
+ result.add("41");
+ result.add("43");
+ result.add("44");
+ result.add("45");
+ result.add("46");
+ result.add("47");
+ result.add("48");
+ result.add("49");
+ result.add("51");
+ result.add("52");
+ result.add("53");
+ result.add("54");
+ result.add("55");
+ result.add("56");
+ result.add("57");
+ result.add("58");
+ result.add("60");
+ result.add("61");
+ result.add("62");
+ result.add("63");
+ result.add("64");
+ result.add("65");
+ result.add("66");
+ result.add("81");
+ result.add("82");
+ result.add("84");
+ result.add("86");
+ result.add("90");
+ result.add("91");
+ result.add("92");
+ result.add("93");
+ result.add("94");
+ result.add("95");
+ result.add("98");
+ result.add("211");
+ result.add("212");
+ result.add("213");
+ result.add("216");
+ result.add("218");
+ result.add("220");
+ result.add("221");
+ result.add("222");
+ result.add("223");
+ result.add("224");
+ result.add("225");
+ result.add("226");
+ result.add("227");
+ result.add("228");
+ result.add("229");
+ result.add("230");
+ result.add("231");
+ result.add("232");
+ result.add("233");
+ result.add("234");
+ result.add("235");
+ result.add("236");
+ result.add("237");
+ result.add("238");
+ result.add("239");
+ result.add("240");
+ result.add("241");
+ result.add("242");
+ result.add("243");
+ result.add("244");
+ result.add("245");
+ result.add("246");
+ result.add("247");
+ result.add("248");
+ result.add("249");
+ result.add("250");
+ result.add("251");
+ result.add("252");
+ result.add("253");
+ result.add("254");
+ result.add("255");
+ result.add("256");
+ result.add("257");
+ result.add("258");
+ result.add("260");
+ result.add("261");
+ result.add("262");
+ result.add("263");
+ result.add("264");
+ result.add("265");
+ result.add("266");
+ result.add("267");
+ result.add("268");
+ result.add("269");
+ result.add("290");
+ result.add("291");
+ result.add("297");
+ result.add("298");
+ result.add("299");
+ result.add("350");
+ result.add("351");
+ result.add("352");
+ result.add("353");
+ result.add("354");
+ result.add("355");
+ result.add("356");
+ result.add("357");
+ result.add("358");
+ result.add("359");
+ result.add("370");
+ result.add("371");
+ result.add("372");
+ result.add("373");
+ result.add("374");
+ result.add("375");
+ result.add("376");
+ result.add("377");
+ result.add("378");
+ result.add("379");
+ result.add("380");
+ result.add("381");
+ result.add("382");
+ result.add("385");
+ result.add("386");
+ result.add("387");
+ result.add("389");
+ result.add("420");
+ result.add("421");
+ result.add("423");
+ result.add("500");
+ result.add("501");
+ result.add("502");
+ result.add("503");
+ result.add("504");
+ result.add("505");
+ result.add("506");
+ result.add("507");
+ result.add("508");
+ result.add("509");
+ result.add("590");
+ result.add("591");
+ result.add("592");
+ result.add("593");
+ result.add("594");
+ result.add("595");
+ result.add("596");
+ result.add("597");
+ result.add("598");
+ result.add("599");
+ result.add("670");
+ result.add("672");
+ result.add("673");
+ result.add("674");
+ result.add("675");
+ result.add("676");
+ result.add("677");
+ result.add("678");
+ result.add("679");
+ result.add("680");
+ result.add("681");
+ result.add("682");
+ result.add("683");
+ result.add("685");
+ result.add("686");
+ result.add("687");
+ result.add("688");
+ result.add("689");
+ result.add("690");
+ result.add("691");
+ result.add("692");
+ result.add("800");
+ result.add("808");
+ result.add("850");
+ result.add("852");
+ result.add("853");
+ result.add("855");
+ result.add("856");
+ result.add("870");
+ result.add("878");
+ result.add("880");
+ result.add("881");
+ result.add("882");
+ result.add("883");
+ result.add("886");
+ result.add("888");
+ result.add("960");
+ result.add("961");
+ result.add("962");
+ result.add("963");
+ result.add("964");
+ result.add("965");
+ result.add("966");
+ result.add("967");
+ result.add("968");
+ result.add("970");
+ result.add("971");
+ result.add("972");
+ result.add("973");
+ result.add("974");
+ result.add("975");
+ result.add("976");
+ result.add("977");
+ result.add("979");
+ result.add("992");
+ result.add("993");
+ result.add("994");
+ result.add("995");
+ result.add("996");
+ result.add("998");
+ return result;
+ }
+
+ public static SmartDialMap getMap() {
+ return mMap;
+ }
+
+ /**
+ * Indicates whether the given country uses NANP numbers
+ *
+ * @param country ISO 3166 country code (case doesn't matter)
+ * @return True if country uses NANP numbers (e.g. US, Canada), false otherwise
+ * @see <a href="https://en.wikipedia.org/wiki/North_American_Numbering_Plan">
+ * https://en.wikipedia.org/wiki/North_American_Numbering_Plan</a>
+ */
+ @VisibleForTesting
+ public static boolean isCountryNanp(String country) {
+ if (TextUtils.isEmpty(country)) {
+ return false;
+ }
+ if (sNanpCountries == null) {
+ sNanpCountries = initNanpCountries();
+ }
+ return sNanpCountries.contains(country.toUpperCase());
+ }
+
+ private static Set<String> initNanpCountries() {
+ final HashSet<String> result = new HashSet<String>();
+ result.add("US"); // United States
+ result.add("CA"); // Canada
+ result.add("AS"); // American Samoa
+ result.add("AI"); // Anguilla
+ result.add("AG"); // Antigua and Barbuda
+ result.add("BS"); // Bahamas
+ result.add("BB"); // Barbados
+ result.add("BM"); // Bermuda
+ result.add("VG"); // British Virgin Islands
+ result.add("KY"); // Cayman Islands
+ result.add("DM"); // Dominica
+ result.add("DO"); // Dominican Republic
+ result.add("GD"); // Grenada
+ result.add("GU"); // Guam
+ result.add("JM"); // Jamaica
+ result.add("PR"); // Puerto Rico
+ result.add("MS"); // Montserrat
+ result.add("MP"); // Northern Mariana Islands
+ result.add("KN"); // Saint Kitts and Nevis
+ result.add("LC"); // Saint Lucia
+ result.add("VC"); // Saint Vincent and the Grenadines
+ result.add("TT"); // Trinidad and Tobago
+ result.add("TC"); // Turks and Caicos Islands
+ result.add("VI"); // U.S. Virgin Islands
+ return result;
+ }
+
+ /**
+ * Returns whether the user is in a region that uses Nanp format based on the sim location.
+ *
+ * @return Whether user is in Nanp region.
+ */
+ public static boolean getUserInNanpRegion() {
+ return sUserInNanpRegion;
+ }
+
+ /** Explicitly setting the user Nanp to the given boolean */
+ @VisibleForTesting
+ public static void setUserInNanpRegion(boolean userInNanpRegion) {
+ sUserInNanpRegion = userInNanpRegion;
+ }
+
+ /** Class to record phone number parsing information. */
+ public static class PhoneNumberTokens {
+
+ /** Country code of the phone number. */
+ final String countryCode;
+
+ /** Offset of national number after the country code. */
+ final int countryCodeOffset;
+
+ /** Offset of local number after NANP area code. */
+ final int nanpCodeOffset;
+
+ public PhoneNumberTokens(String countryCode, int countryCodeOffset, int nanpCodeOffset) {
+ this.countryCode = countryCode;
+ this.countryCodeOffset = countryCodeOffset;
+ this.nanpCodeOffset = nanpCodeOffset;
+ }
+ }
+}