summaryrefslogtreecommitdiff
path: root/java/com/android/dialer/smartdial
diff options
context:
space:
mode:
authorEric Erfanian <erfanian@google.com>2017-02-22 16:32:36 -0800
committerEric Erfanian <erfanian@google.com>2017-03-01 09:56:52 -0800
commitccca31529c07970e89419fb85a9e8153a5396838 (patch)
treea7034c0a01672b97728c13282a2672771cd28baa /java/com/android/dialer/smartdial
parente7ae4624ba6f25cb8e648db74e0d64c0113a16ba (diff)
Update dialer sources.
Test: Built package and system image. This change clobbers the old source, and is an export from an internal Google repository. The internal repository was forked form Android in March, and this change includes modifications since then, to near the v8 release. Since the fork, we've moved code from monolithic to independent modules. In addition, we've switched to Blaze/Bazel as the build sysetm. This export, however, still uses make. New dependencies have been added: - Dagger - Auto-Value - Glide - Libshortcutbadger Going forward, development will still be in Google3, and the Gerrit release will become an automated export, with the next drop happening in ~ two weeks. Android.mk includes local modifications from ToT. Abridged changelog: Bug fixes ● Not able to mute, add a call when using Phone app in multiwindow mode ● Double tap on keypad triggering multiple key and tones ● Reported spam numbers not showing as spam in the call log ● Crash when user tries to block number while Phone app is not set as default ● Crash when user picks a number from search auto-complete list Visual Voicemail (VVM) improvements ● Share Voicemail audio via standard exporting mechanisms that support file attachment (email, MMS, etc.) ● Make phone number, email and web sites in VVM transcript clickable ● Set PIN before declining VVM Terms of Service {Carrier} ● Set client type for outbound visual voicemail SMS {Carrier} New incoming call and incall UI on older devices (Android M) ● Updated Phone app icon ● New incall UI (large buttons, button labels) ● New and animated Answer/Reject gestures Accessibility ● Add custom answer/decline call buttons on answer screen for touch exploration accessibility services ● Increase size of touch target ● Add verbal feedback when a Voicemail fails to load ● Fix pressing of Phone buttons while in a phone call using Switch Access ● Fix selecting and opening contacts in talkback mode ● Split focus for ‘Learn More’ link in caller id & spam to help distinguish similar text Other ● Backup & Restore for App Preferences ● Prompt user to enable Wi-Fi calling if the call ends due to out of service and Wi-Fi is connected ● Rename “Dialpad” to “Keypad” ● Show "Private number" for restricted calls ● Delete unused items (vcard, add contact, call history) from Phone menu Change-Id: I2a7e53532a24c21bf308bf0a6d178d7ddbca4958
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;
+ }
+ }
+}