Support dual alphabets in smart dial.

Bug: 30215380,70633239
Test: CompositeSmartDialMapTest, LatinSmartDialMapTest, RussianSmartDialMapTest, SmartDialNameMatcherTest
PiperOrigin-RevId: 179580982
Change-Id: I5e4c3e61f0dfdc6ca1e80a93bb985ffec08dd8b0
diff --git a/java/com/android/dialer/app/DialtactsActivity.java b/java/com/android/dialer/app/DialtactsActivity.java
index 1a549ab..eb95a4e 100644
--- a/java/com/android/dialer/app/DialtactsActivity.java
+++ b/java/com/android/dialer/app/DialtactsActivity.java
@@ -1400,7 +1400,7 @@
       mNewSearchFragment.setRawNumber(query);
     }
     final String normalizedQuery =
-        SmartDialNameMatcher.normalizeNumber(query, SmartDialNameMatcher.LATIN_SMART_DIAL_MAP);
+        SmartDialNameMatcher.normalizeNumber(/* context = */ this, query);
 
     if (!TextUtils.equals(mSearchView.getText(), normalizedQuery)) {
       if (DEBUG) {
diff --git a/java/com/android/dialer/app/list/SmartDialNumberListAdapter.java b/java/com/android/dialer/app/list/SmartDialNumberListAdapter.java
index 3b00c76..5b48ccf 100644
--- a/java/com/android/dialer/app/list/SmartDialNumberListAdapter.java
+++ b/java/com/android/dialer/app/list/SmartDialNumberListAdapter.java
@@ -25,7 +25,6 @@
 import com.android.dialer.dialpadview.SmartDialCursorLoader;
 import com.android.dialer.smartdial.SmartDialMatchPosition;
 import com.android.dialer.smartdial.SmartDialNameMatcher;
-import com.android.dialer.smartdial.SmartDialPrefix;
 import com.android.dialer.util.CallUtil;
 import java.util.ArrayList;
 
@@ -35,11 +34,13 @@
   private static final String TAG = SmartDialNumberListAdapter.class.getSimpleName();
   private static final boolean DEBUG = false;
 
+  private final Context mContext;
   @NonNull private final SmartDialNameMatcher mNameMatcher;
 
   public SmartDialNumberListAdapter(Context context) {
     super(context);
-    mNameMatcher = new SmartDialNameMatcher("", SmartDialPrefix.getMap());
+    mContext = context;
+    mNameMatcher = new SmartDialNameMatcher("");
     setShortcutEnabled(SmartDialNumberListAdapter.SHORTCUT_DIRECT_CALL, false);
 
     if (DEBUG) {
@@ -72,7 +73,7 @@
   protected void setHighlight(ContactListItemView view, Cursor cursor) {
     view.clearHighlightSequences();
 
-    if (mNameMatcher.matches(cursor.getString(PhoneQuery.DISPLAY_NAME))) {
+    if (mNameMatcher.matches(mContext, cursor.getString(PhoneQuery.DISPLAY_NAME))) {
       final ArrayList<SmartDialMatchPosition> nameMatches = mNameMatcher.getMatchPositions();
       for (SmartDialMatchPosition match : nameMatches) {
         view.addNameHighlightSequence(match.start, match.end);
@@ -89,7 +90,7 @@
     }
 
     final SmartDialMatchPosition numberMatch =
-        mNameMatcher.matchesNumber(cursor.getString(PhoneQuery.PHONE_NUMBER));
+        mNameMatcher.matchesNumber(mContext, cursor.getString(PhoneQuery.PHONE_NUMBER));
     if (numberMatch != null) {
       view.addNumberHighlightSequence(numberMatch.start, numberMatch.end);
     }
diff --git a/java/com/android/dialer/database/DialerDatabaseHelper.java b/java/com/android/dialer/database/DialerDatabaseHelper.java
index 113e863..b0bd62a 100644
--- a/java/com/android/dialer/database/DialerDatabaseHelper.java
+++ b/java/com/android/dialer/database/DialerDatabaseHelper.java
@@ -535,7 +535,7 @@
         insert.executeInsert();
         final String contactPhoneNumber = updatedContactCursor.getString(PhoneQuery.PHONE_NUMBER);
         final ArrayList<String> numberPrefixes =
-            SmartDialPrefix.parseToNumberTokens(contactPhoneNumber);
+            SmartDialPrefix.parseToNumberTokens(mContext, contactPhoneNumber);
 
         for (String numberPrefix : numberPrefixes) {
           numberInsert.bindLong(1, updatedContactCursor.getLong(PhoneQuery.PHONE_CONTACT_ID));
@@ -578,7 +578,7 @@
       while (nameCursor.moveToNext()) {
         /** Computes a list of prefixes of a given contact name. */
         final ArrayList<String> namePrefixes =
-            SmartDialPrefix.generateNamePrefixes(nameCursor.getString(columnIndexName));
+            SmartDialPrefix.generateNamePrefixes(mContext, nameCursor.getString(columnIndexName));
 
         for (String namePrefix : namePrefixes) {
           insert.bindLong(1, nameCursor.getLong(columnIndexContactId));
@@ -912,8 +912,9 @@
         /**
          * If the contact has either the name or number that matches the query, add to the result.
          */
-        final boolean nameMatches = nameMatcher.matches(displayName);
-        final boolean numberMatches = (nameMatcher.matchesNumber(phoneNumber, query) != null);
+        final boolean nameMatches = nameMatcher.matches(mContext, displayName);
+        final boolean numberMatches =
+            (nameMatcher.matchesNumber(mContext, phoneNumber, query) != null);
         if (nameMatches || numberMatches) {
           /** If a contact has not been added, add it to the result and the hash set. */
           duplicates.add(contactMatch);
diff --git a/java/com/android/dialer/dialpadview/SmartDialCursorLoader.java b/java/com/android/dialer/dialpadview/SmartDialCursorLoader.java
index 271535f..d085b55 100644
--- a/java/com/android/dialer/dialpadview/SmartDialCursorLoader.java
+++ b/java/com/android/dialer/dialpadview/SmartDialCursorLoader.java
@@ -26,7 +26,6 @@
 import com.android.dialer.database.DialerDatabaseHelper;
 import com.android.dialer.database.DialerDatabaseHelper.ContactNumber;
 import com.android.dialer.smartdial.SmartDialNameMatcher;
-import com.android.dialer.smartdial.SmartDialPrefix;
 import com.android.dialer.util.PermissionsUtil;
 import java.util.ArrayList;
 
@@ -59,10 +58,10 @@
     if (DEBUG) {
       LogUtil.v(TAG, "Configure new query to be " + query);
     }
-    mQuery = SmartDialNameMatcher.normalizeNumber(query, SmartDialPrefix.getMap());
+    mQuery = SmartDialNameMatcher.normalizeNumber(mContext, query);
 
     /** Constructs a name matcher object for matching names. */
-    mNameMatcher = new SmartDialNameMatcher(mQuery, SmartDialPrefix.getMap());
+    mNameMatcher = new SmartDialNameMatcher(mQuery);
     mNameMatcher.setShouldMatchEmptyQuery(!mShowEmptyListForNullQuery);
   }
 
diff --git a/java/com/android/dialer/smartdial/CompositeSmartDialMap.java b/java/com/android/dialer/smartdial/CompositeSmartDialMap.java
new file mode 100644
index 0000000..d51e46f
--- /dev/null
+++ b/java/com/android/dialer/smartdial/CompositeSmartDialMap.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2017 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.support.annotation.VisibleForTesting;
+import android.support.v4.util.SimpleArrayMap;
+import com.android.dialer.compat.CompatUtils;
+import com.android.dialer.configprovider.ConfigProviderBindings;
+import com.google.common.base.Optional;
+
+/**
+ * A utility class that combines the functionality of two implementations of {@link SmartDialMap} so
+ * that we support smart dial for dual alphabets.
+ *
+ * <p>Of the two implementations of {@link SmartDialMap}, the default one always takes precedence.
+ * The second one is consulted only when the default one is unable to provide a valid result.
+ *
+ * <p>Note that the second implementation can be absent if it is not defined for the system's 1st
+ * language preference.
+ */
+@SuppressWarnings("Guava")
+public class CompositeSmartDialMap {
+  @VisibleForTesting
+  public static final String FLAG_ENABLE_DUAL_ALPHABETS = "enable_dual_alphabets_on_t9";
+
+  private static final SmartDialMap DEFAULT_MAP = LatinSmartDialMap.getInstance();
+
+  // A map in which each key is an ISO 639-2 language code and the corresponding value is a
+  // SmartDialMap
+  private static final SimpleArrayMap<String, SmartDialMap> EXTRA_MAPS = new SimpleArrayMap<>();
+
+  static {
+    EXTRA_MAPS.put("rus", RussianSmartDialMap.getInstance());
+  }
+
+  private CompositeSmartDialMap() {}
+
+  /**
+   * Returns true if the provided character can be mapped to a key on the dialpad.
+   *
+   * <p>The provided character is expected to be a normalized character. See {@link
+   * SmartDialMap#normalizeCharacter(char)} for details.
+   */
+  static boolean isValidDialpadCharacter(Context context, char ch) {
+    if (DEFAULT_MAP.isValidDialpadCharacter(ch)) {
+      return true;
+    }
+
+    Optional<SmartDialMap> extraMap = getExtraMap(context);
+    return extraMap.isPresent() && extraMap.get().isValidDialpadCharacter(ch);
+  }
+
+  /**
+   * Returns true if the provided character is a letter, and can be mapped to a key on the dialpad.
+   *
+   * <p>The provided character is expected to be a normalized character. See {@link
+   * SmartDialMap#normalizeCharacter(char)} for details.
+   */
+  static boolean isValidDialpadAlphabeticChar(Context context, char ch) {
+    if (DEFAULT_MAP.isValidDialpadAlphabeticChar(ch)) {
+      return true;
+    }
+
+    Optional<SmartDialMap> extraMap = getExtraMap(context);
+    return extraMap.isPresent() && extraMap.get().isValidDialpadAlphabeticChar(ch);
+  }
+
+  /**
+   * Returns true if the provided character is a digit, and can be mapped to a key on the dialpad.
+   */
+  static boolean isValidDialpadNumericChar(Context context, char ch) {
+    if (DEFAULT_MAP.isValidDialpadNumericChar(ch)) {
+      return true;
+    }
+
+    Optional<SmartDialMap> extraMap = getExtraMap(context);
+    return extraMap.isPresent() && extraMap.get().isValidDialpadNumericChar(ch);
+  }
+
+  /**
+   * Get the index of the key on the dialpad which the character corresponds to.
+   *
+   * <p>The provided character is expected to be a normalized character. See {@link
+   * SmartDialMap#normalizeCharacter(char)} for details.
+   *
+   * <p>If the provided character can't be mapped to a key on the dialpad, return -1.
+   */
+  static byte getDialpadIndex(Context context, char ch) {
+    Optional<Byte> dialpadIndex = DEFAULT_MAP.getDialpadIndex(ch);
+    if (dialpadIndex.isPresent()) {
+      return dialpadIndex.get();
+    }
+
+    Optional<SmartDialMap> extraMap = getExtraMap(context);
+    if (extraMap.isPresent()) {
+      dialpadIndex = extraMap.get().getDialpadIndex(ch);
+    }
+
+    return dialpadIndex.isPresent() ? dialpadIndex.get() : -1;
+  }
+
+  /**
+   * Get the actual numeric character on the dialpad which the character corresponds to.
+   *
+   * <p>The provided character is expected to be a normalized character. See {@link
+   * SmartDialMap#normalizeCharacter(char)} for details.
+   *
+   * <p>If the provided character can't be mapped to a key on the dialpad, return the character.
+   */
+  static char getDialpadNumericCharacter(Context context, char ch) {
+    Optional<Character> dialpadNumericChar = DEFAULT_MAP.getDialpadNumericCharacter(ch);
+    if (dialpadNumericChar.isPresent()) {
+      return dialpadNumericChar.get();
+    }
+
+    Optional<SmartDialMap> extraMap = getExtraMap(context);
+    if (extraMap.isPresent()) {
+      dialpadNumericChar = extraMap.get().getDialpadNumericCharacter(ch);
+    }
+
+    return dialpadNumericChar.isPresent() ? dialpadNumericChar.get() : ch;
+  }
+
+  /**
+   * Converts uppercase characters to lower case ones, and on a best effort basis, strips accents
+   * from accented characters.
+   *
+   * <p>If the provided character can't be mapped to a key on the dialpad, return the character.
+   */
+  static char normalizeCharacter(Context context, char ch) {
+    Optional<Character> normalizedChar = DEFAULT_MAP.normalizeCharacter(ch);
+    if (normalizedChar.isPresent()) {
+      return normalizedChar.get();
+    }
+
+    Optional<SmartDialMap> extraMap = getExtraMap(context);
+    if (extraMap.isPresent()) {
+      normalizedChar = extraMap.get().normalizeCharacter(ch);
+    }
+
+    return normalizedChar.isPresent() ? normalizedChar.get() : ch;
+  }
+
+  @VisibleForTesting
+  static Optional<SmartDialMap> getExtraMap(Context context) {
+    if (!ConfigProviderBindings.get(context).getBoolean(FLAG_ENABLE_DUAL_ALPHABETS, false)) {
+      return Optional.absent();
+    }
+
+    String languageCode = CompatUtils.getLocale(context).getISO3Language();
+    return EXTRA_MAPS.containsKey(languageCode)
+        ? Optional.of(EXTRA_MAPS.get(languageCode))
+        : Optional.absent();
+  }
+}
diff --git a/java/com/android/dialer/smartdial/LatinSmartDialMap.java b/java/com/android/dialer/smartdial/LatinSmartDialMap.java
index 656fd12..b67901b 100644
--- a/java/com/android/dialer/smartdial/LatinSmartDialMap.java
+++ b/java/com/android/dialer/smartdial/LatinSmartDialMap.java
@@ -16,52 +16,62 @@
 
 package com.android.dialer.smartdial;
 
-/** {@link SmartDialMap} for Latin based T9 dialpad searching. */
-public class LatinSmartDialMap implements SmartDialMap {
+import android.support.v4.util.SimpleArrayMap;
+import com.google.common.base.Optional;
 
-  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
-  };
+/** A {@link SmartDialMap} for the Latin alphabet, which is for T9 dialpad searching. */
+@SuppressWarnings("Guava")
+final class LatinSmartDialMap extends SmartDialMap {
+  private static final SimpleArrayMap<Character, Character> CHAR_TO_KEY_MAP =
+      new SimpleArrayMap<>();
 
-  @Override
-  public boolean isValidDialpadAlphabeticChar(char ch) {
-    return (ch >= 'a' && ch <= 'z');
+  static {
+    CHAR_TO_KEY_MAP.put('a', '2');
+    CHAR_TO_KEY_MAP.put('b', '2');
+    CHAR_TO_KEY_MAP.put('c', '2');
+
+    CHAR_TO_KEY_MAP.put('d', '3');
+    CHAR_TO_KEY_MAP.put('e', '3');
+    CHAR_TO_KEY_MAP.put('f', '3');
+
+    CHAR_TO_KEY_MAP.put('g', '4');
+    CHAR_TO_KEY_MAP.put('h', '4');
+    CHAR_TO_KEY_MAP.put('i', '4');
+
+    CHAR_TO_KEY_MAP.put('j', '5');
+    CHAR_TO_KEY_MAP.put('k', '5');
+    CHAR_TO_KEY_MAP.put('l', '5');
+
+    CHAR_TO_KEY_MAP.put('m', '6');
+    CHAR_TO_KEY_MAP.put('n', '6');
+    CHAR_TO_KEY_MAP.put('o', '6');
+
+    CHAR_TO_KEY_MAP.put('p', '7');
+    CHAR_TO_KEY_MAP.put('q', '7');
+    CHAR_TO_KEY_MAP.put('r', '7');
+    CHAR_TO_KEY_MAP.put('s', '7');
+
+    CHAR_TO_KEY_MAP.put('t', '8');
+    CHAR_TO_KEY_MAP.put('u', '8');
+    CHAR_TO_KEY_MAP.put('v', '8');
+
+    CHAR_TO_KEY_MAP.put('w', '9');
+    CHAR_TO_KEY_MAP.put('x', '9');
+    CHAR_TO_KEY_MAP.put('y', '9');
+    CHAR_TO_KEY_MAP.put('z', '9');
   }
 
-  @Override
-  public boolean isValidDialpadNumericChar(char ch) {
-    return (ch >= '0' && ch <= '9');
+  private static LatinSmartDialMap instance;
+
+  static LatinSmartDialMap getInstance() {
+    if (instance == null) {
+      instance = new LatinSmartDialMap();
+    }
+
+    return instance;
   }
 
-  @Override
-  public boolean isValidDialpadCharacter(char ch) {
-    return (isValidDialpadAlphabeticChar(ch) || isValidDialpadNumericChar(ch));
-  }
+  private LatinSmartDialMap() {}
 
   /*
    * The switch statement in this function was generated using the python code:
@@ -72,7 +82,7 @@
    *     # 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) +  "';"
+   *         print "case '" + char + "': return Optional.of('" + unidecode(char) +  "');"
    *
    * This gives us a way to map characters containing accents/diacritics to their
    * alphabetic equivalents. The unidecode library can be found at:
@@ -81,705 +91,695 @@
    * Also remaps all upper case latin characters to their lower case equivalents.
    */
   @Override
-  public char normalizeCharacter(char ch) {
+  Optional<Character> normalizeCharacter(char ch) {
+    if (isValidDialpadAlphabeticChar(ch)) {
+      return Optional.of(ch);
+    }
+
     switch (ch) {
       case 'À':
-        return 'a';
+        return Optional.of('a');
       case 'Á':
-        return 'a';
+        return Optional.of('a');
       case 'Â':
-        return 'a';
+        return Optional.of('a');
       case 'Ã':
-        return 'a';
+        return Optional.of('a');
       case 'Ä':
-        return 'a';
+        return Optional.of('a');
       case 'Å':
-        return 'a';
+        return Optional.of('a');
       case 'Ç':
-        return 'c';
+        return Optional.of('c');
       case 'È':
-        return 'e';
+        return Optional.of('e');
       case 'É':
-        return 'e';
+        return Optional.of('e');
       case 'Ê':
-        return 'e';
+        return Optional.of('e');
       case 'Ë':
-        return 'e';
+        return Optional.of('e');
       case 'Ì':
-        return 'i';
+        return Optional.of('i');
       case 'Í':
-        return 'i';
+        return Optional.of('i');
       case 'Î':
-        return 'i';
+        return Optional.of('i');
       case 'Ï':
-        return 'i';
+        return Optional.of('i');
       case 'Ð':
-        return 'd';
+        return Optional.of('d');
       case 'Ñ':
-        return 'n';
+        return Optional.of('n');
       case 'Ò':
-        return 'o';
+        return Optional.of('o');
       case 'Ó':
-        return 'o';
+        return Optional.of('o');
       case 'Ô':
-        return 'o';
+        return Optional.of('o');
       case 'Õ':
-        return 'o';
+        return Optional.of('o');
       case 'Ö':
-        return 'o';
+        return Optional.of('o');
       case '×':
-        return 'x';
+        return Optional.of('x');
       case 'Ø':
-        return 'o';
+        return Optional.of('o');
       case 'Ù':
-        return 'u';
+        return Optional.of('u');
       case 'Ú':
-        return 'u';
+        return Optional.of('u');
       case 'Û':
-        return 'u';
+        return Optional.of('u');
       case 'Ü':
-        return 'u';
+        return Optional.of('u');
       case 'Ý':
-        return 'u';
+        return Optional.of('u');
       case 'à':
-        return 'a';
+        return Optional.of('a');
       case 'á':
-        return 'a';
+        return Optional.of('a');
       case 'â':
-        return 'a';
+        return Optional.of('a');
       case 'ã':
-        return 'a';
+        return Optional.of('a');
       case 'ä':
-        return 'a';
+        return Optional.of('a');
       case 'å':
-        return 'a';
+        return Optional.of('a');
       case 'ç':
-        return 'c';
+        return Optional.of('c');
       case 'è':
-        return 'e';
+        return Optional.of('e');
       case 'é':
-        return 'e';
+        return Optional.of('e');
       case 'ê':
-        return 'e';
+        return Optional.of('e');
       case 'ë':
-        return 'e';
+        return Optional.of('e');
       case 'ì':
-        return 'i';
+        return Optional.of('i');
       case 'í':
-        return 'i';
+        return Optional.of('i');
       case 'î':
-        return 'i';
+        return Optional.of('i');
       case 'ï':
-        return 'i';
+        return Optional.of('i');
       case 'ð':
-        return 'd';
+        return Optional.of('d');
       case 'ñ':
-        return 'n';
+        return Optional.of('n');
       case 'ò':
-        return 'o';
+        return Optional.of('o');
       case 'ó':
-        return 'o';
+        return Optional.of('o');
       case 'ô':
-        return 'o';
+        return Optional.of('o');
       case 'õ':
-        return 'o';
+        return Optional.of('o');
       case 'ö':
-        return 'o';
+        return Optional.of('o');
       case 'ø':
-        return 'o';
+        return Optional.of('o');
       case 'ù':
-        return 'u';
+        return Optional.of('u');
       case 'ú':
-        return 'u';
+        return Optional.of('u');
       case 'û':
-        return 'u';
+        return Optional.of('u');
       case 'ü':
-        return 'u';
+        return Optional.of('u');
       case 'ý':
-        return 'y';
+        return Optional.of('y');
       case 'ÿ':
-        return 'y';
+        return Optional.of('y');
       case 'Ā':
-        return 'a';
+        return Optional.of('a');
       case 'ā':
-        return 'a';
+        return Optional.of('a');
       case 'Ă':
-        return 'a';
+        return Optional.of('a');
       case 'ă':
-        return 'a';
+        return Optional.of('a');
       case 'Ą':
-        return 'a';
+        return Optional.of('a');
       case 'ą':
-        return 'a';
+        return Optional.of('a');
       case 'Ć':
-        return 'c';
+        return Optional.of('c');
       case 'ć':
-        return 'c';
+        return Optional.of('c');
       case 'Ĉ':
-        return 'c';
+        return Optional.of('c');
       case 'ĉ':
-        return 'c';
+        return Optional.of('c');
       case 'Ċ':
-        return 'c';
+        return Optional.of('c');
       case 'ċ':
-        return 'c';
+        return Optional.of('c');
       case 'Č':
-        return 'c';
+        return Optional.of('c');
       case 'č':
-        return 'c';
+        return Optional.of('c');
       case 'Ď':
-        return 'd';
+        return Optional.of('d');
       case 'ď':
-        return 'd';
+        return Optional.of('d');
       case 'Đ':
-        return 'd';
+        return Optional.of('d');
       case 'đ':
-        return 'd';
+        return Optional.of('d');
       case 'Ē':
-        return 'e';
+        return Optional.of('e');
       case 'ē':
-        return 'e';
+        return Optional.of('e');
       case 'Ĕ':
-        return 'e';
+        return Optional.of('e');
       case 'ĕ':
-        return 'e';
+        return Optional.of('e');
       case 'Ė':
-        return 'e';
+        return Optional.of('e');
       case 'ė':
-        return 'e';
+        return Optional.of('e');
       case 'Ę':
-        return 'e';
+        return Optional.of('e');
       case 'ę':
-        return 'e';
+        return Optional.of('e');
       case 'Ě':
-        return 'e';
+        return Optional.of('e');
       case 'ě':
-        return 'e';
+        return Optional.of('e');
       case 'Ĝ':
-        return 'g';
+        return Optional.of('g');
       case 'ĝ':
-        return 'g';
+        return Optional.of('g');
       case 'Ğ':
-        return 'g';
+        return Optional.of('g');
       case 'ğ':
-        return 'g';
+        return Optional.of('g');
       case 'Ġ':
-        return 'g';
+        return Optional.of('g');
       case 'ġ':
-        return 'g';
+        return Optional.of('g');
       case 'Ģ':
-        return 'g';
+        return Optional.of('g');
       case 'ģ':
-        return 'g';
+        return Optional.of('g');
       case 'Ĥ':
-        return 'h';
+        return Optional.of('h');
       case 'ĥ':
-        return 'h';
+        return Optional.of('h');
       case 'Ħ':
-        return 'h';
+        return Optional.of('h');
       case 'ħ':
-        return 'h';
+        return Optional.of('h');
       case 'Ĩ':
-        return 'i';
+        return Optional.of('i');
       case 'ĩ':
-        return 'i';
+        return Optional.of('i');
       case 'Ī':
-        return 'i';
+        return Optional.of('i');
       case 'ī':
-        return 'i';
+        return Optional.of('i');
       case 'Ĭ':
-        return 'i';
+        return Optional.of('i');
       case 'ĭ':
-        return 'i';
+        return Optional.of('i');
       case 'Į':
-        return 'i';
+        return Optional.of('i');
       case 'į':
-        return 'i';
+        return Optional.of('i');
       case 'İ':
-        return 'i';
+        return Optional.of('i');
       case 'ı':
-        return 'i';
+        return Optional.of('i');
       case 'Ĵ':
-        return 'j';
+        return Optional.of('j');
       case 'ĵ':
-        return 'j';
+        return Optional.of('j');
       case 'Ķ':
-        return 'k';
+        return Optional.of('k');
       case 'ķ':
-        return 'k';
+        return Optional.of('k');
       case 'ĸ':
-        return 'k';
+        return Optional.of('k');
       case 'Ĺ':
-        return 'l';
+        return Optional.of('l');
       case 'ĺ':
-        return 'l';
+        return Optional.of('l');
       case 'Ļ':
-        return 'l';
+        return Optional.of('l');
       case 'ļ':
-        return 'l';
+        return Optional.of('l');
       case 'Ľ':
-        return 'l';
+        return Optional.of('l');
       case 'ľ':
-        return 'l';
+        return Optional.of('l');
       case 'Ŀ':
-        return 'l';
+        return Optional.of('l');
       case 'ŀ':
-        return 'l';
+        return Optional.of('l');
       case 'Ł':
-        return 'l';
+        return Optional.of('l');
       case 'ł':
-        return 'l';
+        return Optional.of('l');
       case 'Ń':
-        return 'n';
+        return Optional.of('n');
       case 'ń':
-        return 'n';
+        return Optional.of('n');
       case 'Ņ':
-        return 'n';
+        return Optional.of('n');
       case 'ņ':
-        return 'n';
+        return Optional.of('n');
       case 'Ň':
-        return 'n';
+        return Optional.of('n');
       case 'ň':
-        return 'n';
+        return Optional.of('n');
       case 'Ō':
-        return 'o';
+        return Optional.of('o');
       case 'ō':
-        return 'o';
+        return Optional.of('o');
       case 'Ŏ':
-        return 'o';
+        return Optional.of('o');
       case 'ŏ':
-        return 'o';
+        return Optional.of('o');
       case 'Ő':
-        return 'o';
+        return Optional.of('o');
       case 'ő':
-        return 'o';
+        return Optional.of('o');
       case 'Ŕ':
-        return 'r';
+        return Optional.of('r');
       case 'ŕ':
-        return 'r';
+        return Optional.of('r');
       case 'Ŗ':
-        return 'r';
+        return Optional.of('r');
       case 'ŗ':
-        return 'r';
+        return Optional.of('r');
       case 'Ř':
-        return 'r';
+        return Optional.of('r');
       case 'ř':
-        return 'r';
+        return Optional.of('r');
       case 'Ś':
-        return 's';
+        return Optional.of('s');
       case 'ś':
-        return 's';
+        return Optional.of('s');
       case 'Ŝ':
-        return 's';
+        return Optional.of('s');
       case 'ŝ':
-        return 's';
+        return Optional.of('s');
       case 'Ş':
-        return 's';
+        return Optional.of('s');
       case 'ş':
-        return 's';
+        return Optional.of('s');
       case 'Š':
-        return 's';
+        return Optional.of('s');
       case 'š':
-        return 's';
+        return Optional.of('s');
       case 'Ţ':
-        return 't';
+        return Optional.of('t');
       case 'ţ':
-        return 't';
+        return Optional.of('t');
       case 'Ť':
-        return 't';
+        return Optional.of('t');
       case 'ť':
-        return 't';
+        return Optional.of('t');
       case 'Ŧ':
-        return 't';
+        return Optional.of('t');
       case 'ŧ':
-        return 't';
+        return Optional.of('t');
       case 'Ũ':
-        return 'u';
+        return Optional.of('u');
       case 'ũ':
-        return 'u';
+        return Optional.of('u');
       case 'Ū':
-        return 'u';
+        return Optional.of('u');
       case 'ū':
-        return 'u';
+        return Optional.of('u');
       case 'Ŭ':
-        return 'u';
+        return Optional.of('u');
       case 'ŭ':
-        return 'u';
+        return Optional.of('u');
       case 'Ů':
-        return 'u';
+        return Optional.of('u');
       case 'ů':
-        return 'u';
+        return Optional.of('u');
       case 'Ű':
-        return 'u';
+        return Optional.of('u');
       case 'ű':
-        return 'u';
+        return Optional.of('u');
       case 'Ų':
-        return 'u';
+        return Optional.of('u');
       case 'ų':
-        return 'u';
+        return Optional.of('u');
       case 'Ŵ':
-        return 'w';
+        return Optional.of('w');
       case 'ŵ':
-        return 'w';
+        return Optional.of('w');
       case 'Ŷ':
-        return 'y';
+        return Optional.of('y');
       case 'ŷ':
-        return 'y';
+        return Optional.of('y');
       case 'Ÿ':
-        return 'y';
+        return Optional.of('y');
       case 'Ź':
-        return 'z';
+        return Optional.of('z');
       case 'ź':
-        return 'z';
+        return Optional.of('z');
       case 'Ż':
-        return 'z';
+        return Optional.of('z');
       case 'ż':
-        return 'z';
+        return Optional.of('z');
       case 'Ž':
-        return 'z';
+        return Optional.of('z');
       case 'ž':
-        return 'z';
+        return Optional.of('z');
       case 'ſ':
-        return 's';
+        return Optional.of('s');
       case 'ƀ':
-        return 'b';
+        return Optional.of('b');
       case 'Ɓ':
-        return 'b';
+        return Optional.of('b');
       case 'Ƃ':
-        return 'b';
+        return Optional.of('b');
       case 'ƃ':
-        return 'b';
+        return Optional.of('b');
       case 'Ɔ':
-        return 'o';
+        return Optional.of('o');
       case 'Ƈ':
-        return 'c';
+        return Optional.of('c');
       case 'ƈ':
-        return 'c';
+        return Optional.of('c');
       case 'Ɖ':
-        return 'd';
+        return Optional.of('d');
       case 'Ɗ':
-        return 'd';
+        return Optional.of('d');
       case 'Ƌ':
-        return 'd';
+        return Optional.of('d');
       case 'ƌ':
-        return 'd';
+        return Optional.of('d');
       case 'ƍ':
-        return 'd';
+        return Optional.of('d');
       case 'Ɛ':
-        return 'e';
+        return Optional.of('e');
       case 'Ƒ':
-        return 'f';
+        return Optional.of('f');
       case 'ƒ':
-        return 'f';
+        return Optional.of('f');
       case 'Ɠ':
-        return 'g';
+        return Optional.of('g');
       case 'Ɣ':
-        return 'g';
+        return Optional.of('g');
       case 'Ɩ':
-        return 'i';
+        return Optional.of('i');
       case 'Ɨ':
-        return 'i';
+        return Optional.of('i');
       case 'Ƙ':
-        return 'k';
+        return Optional.of('k');
       case 'ƙ':
-        return 'k';
+        return Optional.of('k');
       case 'ƚ':
-        return 'l';
+        return Optional.of('l');
       case 'ƛ':
-        return 'l';
+        return Optional.of('l');
       case 'Ɯ':
-        return 'w';
+        return Optional.of('w');
       case 'Ɲ':
-        return 'n';
+        return Optional.of('n');
       case 'ƞ':
-        return 'n';
+        return Optional.of('n');
       case 'Ɵ':
-        return 'o';
+        return Optional.of('o');
       case 'Ơ':
-        return 'o';
+        return Optional.of('o');
       case 'ơ':
-        return 'o';
+        return Optional.of('o');
       case 'Ƥ':
-        return 'p';
+        return Optional.of('p');
       case 'ƥ':
-        return 'p';
+        return Optional.of('p');
       case 'ƫ':
-        return 't';
+        return Optional.of('t');
       case 'Ƭ':
-        return 't';
+        return Optional.of('t');
       case 'ƭ':
-        return 't';
+        return Optional.of('t');
       case 'Ʈ':
-        return 't';
+        return Optional.of('t');
       case 'Ư':
-        return 'u';
+        return Optional.of('u');
       case 'ư':
-        return 'u';
+        return Optional.of('u');
       case 'Ʊ':
-        return 'y';
+        return Optional.of('y');
       case 'Ʋ':
-        return 'v';
+        return Optional.of('v');
       case 'Ƴ':
-        return 'y';
+        return Optional.of('y');
       case 'ƴ':
-        return 'y';
+        return Optional.of('y');
       case 'Ƶ':
-        return 'z';
+        return Optional.of('z');
       case 'ƶ':
-        return 'z';
+        return Optional.of('z');
       case 'ƿ':
-        return 'w';
+        return Optional.of('w');
       case 'Ǎ':
-        return 'a';
+        return Optional.of('a');
       case 'ǎ':
-        return 'a';
+        return Optional.of('a');
       case 'Ǐ':
-        return 'i';
+        return Optional.of('i');
       case 'ǐ':
-        return 'i';
+        return Optional.of('i');
       case 'Ǒ':
-        return 'o';
+        return Optional.of('o');
       case 'ǒ':
-        return 'o';
+        return Optional.of('o');
       case 'Ǔ':
-        return 'u';
+        return Optional.of('u');
       case 'ǔ':
-        return 'u';
+        return Optional.of('u');
       case 'Ǖ':
-        return 'u';
+        return Optional.of('u');
       case 'ǖ':
-        return 'u';
+        return Optional.of('u');
       case 'Ǘ':
-        return 'u';
+        return Optional.of('u');
       case 'ǘ':
-        return 'u';
+        return Optional.of('u');
       case 'Ǚ':
-        return 'u';
+        return Optional.of('u');
       case 'ǚ':
-        return 'u';
+        return Optional.of('u');
       case 'Ǜ':
-        return 'u';
+        return Optional.of('u');
       case 'ǜ':
-        return 'u';
+        return Optional.of('u');
       case 'Ǟ':
-        return 'a';
+        return Optional.of('a');
       case 'ǟ':
-        return 'a';
+        return Optional.of('a');
       case 'Ǡ':
-        return 'a';
+        return Optional.of('a');
       case 'ǡ':
-        return 'a';
+        return Optional.of('a');
       case 'Ǥ':
-        return 'g';
+        return Optional.of('g');
       case 'ǥ':
-        return 'g';
+        return Optional.of('g');
       case 'Ǧ':
-        return 'g';
+        return Optional.of('g');
       case 'ǧ':
-        return 'g';
+        return Optional.of('g');
       case 'Ǩ':
-        return 'k';
+        return Optional.of('k');
       case 'ǩ':
-        return 'k';
+        return Optional.of('k');
       case 'Ǫ':
-        return 'o';
+        return Optional.of('o');
       case 'ǫ':
-        return 'o';
+        return Optional.of('o');
       case 'Ǭ':
-        return 'o';
+        return Optional.of('o');
       case 'ǭ':
-        return 'o';
+        return Optional.of('o');
       case 'ǰ':
-        return 'j';
+        return Optional.of('j');
       case 'Dz':
-        return 'd';
+        return Optional.of('d');
       case 'Ǵ':
-        return 'g';
+        return Optional.of('g');
       case 'ǵ':
-        return 'g';
+        return Optional.of('g');
       case 'Ƿ':
-        return 'w';
+        return Optional.of('w');
       case 'Ǹ':
-        return 'n';
+        return Optional.of('n');
       case 'ǹ':
-        return 'n';
+        return Optional.of('n');
       case 'Ǻ':
-        return 'a';
+        return Optional.of('a');
       case 'ǻ':
-        return 'a';
+        return Optional.of('a');
       case 'Ǿ':
-        return 'o';
+        return Optional.of('o');
       case 'ǿ':
-        return 'o';
+        return Optional.of('o');
       case 'Ȁ':
-        return 'a';
+        return Optional.of('a');
       case 'ȁ':
-        return 'a';
+        return Optional.of('a');
       case 'Ȃ':
-        return 'a';
+        return Optional.of('a');
       case 'ȃ':
-        return 'a';
+        return Optional.of('a');
       case 'Ȅ':
-        return 'e';
+        return Optional.of('e');
       case 'ȅ':
-        return 'e';
+        return Optional.of('e');
       case 'Ȇ':
-        return 'e';
+        return Optional.of('e');
       case 'ȇ':
-        return 'e';
+        return Optional.of('e');
       case 'Ȉ':
-        return 'i';
+        return Optional.of('i');
       case 'ȉ':
-        return 'i';
+        return Optional.of('i');
       case 'Ȋ':
-        return 'i';
+        return Optional.of('i');
       case 'ȋ':
-        return 'i';
+        return Optional.of('i');
       case 'Ȍ':
-        return 'o';
+        return Optional.of('o');
       case 'ȍ':
-        return 'o';
+        return Optional.of('o');
       case 'Ȏ':
-        return 'o';
+        return Optional.of('o');
       case 'ȏ':
-        return 'o';
+        return Optional.of('o');
       case 'Ȑ':
-        return 'r';
+        return Optional.of('r');
       case 'ȑ':
-        return 'r';
+        return Optional.of('r');
       case 'Ȓ':
-        return 'r';
+        return Optional.of('r');
       case 'ȓ':
-        return 'r';
+        return Optional.of('r');
       case 'Ȕ':
-        return 'u';
+        return Optional.of('u');
       case 'ȕ':
-        return 'u';
+        return Optional.of('u');
       case 'Ȗ':
-        return 'u';
+        return Optional.of('u');
       case 'ȗ':
-        return 'u';
+        return Optional.of('u');
       case 'Ș':
-        return 's';
+        return Optional.of('s');
       case 'ș':
-        return 's';
+        return Optional.of('s');
       case 'Ț':
-        return 't';
+        return Optional.of('t');
       case 'ț':
-        return 't';
+        return Optional.of('t');
       case 'Ȝ':
-        return 'y';
+        return Optional.of('y');
       case 'ȝ':
-        return 'y';
+        return Optional.of('y');
       case 'Ȟ':
-        return 'h';
+        return Optional.of('h');
       case 'ȟ':
-        return 'h';
+        return Optional.of('h');
       case 'Ȥ':
-        return 'z';
+        return Optional.of('z');
       case 'ȥ':
-        return 'z';
+        return Optional.of('z');
       case 'Ȧ':
-        return 'a';
+        return Optional.of('a');
       case 'ȧ':
-        return 'a';
+        return Optional.of('a');
       case 'Ȩ':
-        return 'e';
+        return Optional.of('e');
       case 'ȩ':
-        return 'e';
+        return Optional.of('e');
       case 'Ȫ':
-        return 'o';
+        return Optional.of('o');
       case 'ȫ':
-        return 'o';
+        return Optional.of('o');
       case 'Ȭ':
-        return 'o';
+        return Optional.of('o');
       case 'ȭ':
-        return 'o';
+        return Optional.of('o');
       case 'Ȯ':
-        return 'o';
+        return Optional.of('o');
       case 'ȯ':
-        return 'o';
+        return Optional.of('o');
       case 'Ȱ':
-        return 'o';
+        return Optional.of('o');
       case 'ȱ':
-        return 'o';
+        return Optional.of('o');
       case 'Ȳ':
-        return 'y';
+        return Optional.of('y');
       case 'ȳ':
-        return 'y';
+        return Optional.of('y');
       case 'A':
-        return 'a';
+        return Optional.of('a');
       case 'B':
-        return 'b';
+        return Optional.of('b');
       case 'C':
-        return 'c';
+        return Optional.of('c');
       case 'D':
-        return 'd';
+        return Optional.of('d');
       case 'E':
-        return 'e';
+        return Optional.of('e');
       case 'F':
-        return 'f';
+        return Optional.of('f');
       case 'G':
-        return 'g';
+        return Optional.of('g');
       case 'H':
-        return 'h';
+        return Optional.of('h');
       case 'I':
-        return 'i';
+        return Optional.of('i');
       case 'J':
-        return 'j';
+        return Optional.of('j');
       case 'K':
-        return 'k';
+        return Optional.of('k');
       case 'L':
-        return 'l';
+        return Optional.of('l');
       case 'M':
-        return 'm';
+        return Optional.of('m');
       case 'N':
-        return 'n';
+        return Optional.of('n');
       case 'O':
-        return 'o';
+        return Optional.of('o');
       case 'P':
-        return 'p';
+        return Optional.of('p');
       case 'Q':
-        return 'q';
+        return Optional.of('q');
       case 'R':
-        return 'r';
+        return Optional.of('r');
       case 'S':
-        return 's';
+        return Optional.of('s');
       case 'T':
-        return 't';
+        return Optional.of('t');
       case 'U':
-        return 'u';
+        return Optional.of('u');
       case 'V':
-        return 'v';
+        return Optional.of('v');
       case 'W':
-        return 'w';
+        return Optional.of('w');
       case 'X':
-        return 'x';
+        return Optional.of('x');
       case 'Y':
-        return 'y';
+        return Optional.of('y');
       case 'Z':
-        return 'z';
+        return Optional.of('z');
       default:
-        return ch;
+        return Optional.absent();
     }
   }
 
   @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;
+  SimpleArrayMap<Character, Character> getCharToKeyMap() {
+    return CHAR_TO_KEY_MAP;
   }
 }
diff --git a/java/com/android/dialer/smartdial/RussianSmartDialMap.java b/java/com/android/dialer/smartdial/RussianSmartDialMap.java
new file mode 100644
index 0000000..ada9182
--- /dev/null
+++ b/java/com/android/dialer/smartdial/RussianSmartDialMap.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2017 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.v4.util.SimpleArrayMap;
+import com.google.common.base.Optional;
+
+/** A {@link SmartDialMap} for the Russian alphabet. */
+@SuppressWarnings("Guava")
+final class RussianSmartDialMap extends SmartDialMap {
+  private static final SimpleArrayMap<Character, Character> CHAR_TO_KEY_MAP =
+      new SimpleArrayMap<>();
+
+  // Reference: https://en.wikipedia.org/wiki/Russian_alphabet
+  static {
+    CHAR_TO_KEY_MAP.put('а', '2');
+    CHAR_TO_KEY_MAP.put('б', '2');
+    CHAR_TO_KEY_MAP.put('в', '2');
+    CHAR_TO_KEY_MAP.put('г', '2');
+
+    CHAR_TO_KEY_MAP.put('д', '3');
+    CHAR_TO_KEY_MAP.put('е', '3');
+    CHAR_TO_KEY_MAP.put('ё', '3');
+    CHAR_TO_KEY_MAP.put('ж', '3');
+    CHAR_TO_KEY_MAP.put('з', '3');
+
+    CHAR_TO_KEY_MAP.put('и', '4');
+    CHAR_TO_KEY_MAP.put('й', '4');
+    CHAR_TO_KEY_MAP.put('к', '4');
+    CHAR_TO_KEY_MAP.put('л', '4');
+
+    CHAR_TO_KEY_MAP.put('м', '5');
+    CHAR_TO_KEY_MAP.put('н', '5');
+    CHAR_TO_KEY_MAP.put('о', '5');
+    CHAR_TO_KEY_MAP.put('п', '5');
+
+    CHAR_TO_KEY_MAP.put('р', '6');
+    CHAR_TO_KEY_MAP.put('с', '6');
+    CHAR_TO_KEY_MAP.put('т', '6');
+    CHAR_TO_KEY_MAP.put('у', '6');
+
+    CHAR_TO_KEY_MAP.put('ф', '7');
+    CHAR_TO_KEY_MAP.put('х', '7');
+    CHAR_TO_KEY_MAP.put('ц', '7');
+    CHAR_TO_KEY_MAP.put('ч', '7');
+
+    CHAR_TO_KEY_MAP.put('ш', '8');
+    CHAR_TO_KEY_MAP.put('щ', '8');
+    CHAR_TO_KEY_MAP.put('ъ', '8');
+    CHAR_TO_KEY_MAP.put('ы', '8');
+
+    CHAR_TO_KEY_MAP.put('ь', '9');
+    CHAR_TO_KEY_MAP.put('э', '9');
+    CHAR_TO_KEY_MAP.put('ю', '9');
+    CHAR_TO_KEY_MAP.put('я', '9');
+  }
+
+  private static RussianSmartDialMap instance;
+
+  static RussianSmartDialMap getInstance() {
+    if (instance == null) {
+      instance = new RussianSmartDialMap();
+    }
+
+    return instance;
+  }
+
+  private RussianSmartDialMap() {}
+
+  @Override
+  Optional<Character> normalizeCharacter(char ch) {
+    ch = Character.toLowerCase(ch);
+    return isValidDialpadAlphabeticChar(ch) ? Optional.of(ch) : Optional.absent();
+  }
+
+  @Override
+  SimpleArrayMap<Character, Character> getCharToKeyMap() {
+    return CHAR_TO_KEY_MAP;
+  }
+}
diff --git a/java/com/android/dialer/smartdial/SmartDialMap.java b/java/com/android/dialer/smartdial/SmartDialMap.java
index 9638929..bc5c9ea 100644
--- a/java/com/android/dialer/smartdial/SmartDialMap.java
+++ b/java/com/android/dialer/smartdial/SmartDialMap.java
@@ -16,45 +16,88 @@
 
 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 {
+import android.support.v4.util.SimpleArrayMap;
+import com.google.common.base.Optional;
 
-  /*
-   * Returns true if the provided character can be mapped to a key on the dialpad
+/** Definition for utilities that supports smart dial in different languages. */
+@SuppressWarnings("Guava")
+abstract class SmartDialMap {
+
+  /**
+   * Returns true if the provided character can be mapped to a key on the dialpad.
+   *
+   * <p>The provided character is expected to be a normalized character. See {@link
+   * SmartDialMap#normalizeCharacter(char)} for details.
    */
-  boolean isValidDialpadCharacter(char ch);
+  protected boolean isValidDialpadCharacter(char ch) {
+    return isValidDialpadAlphabeticChar(ch) || isValidDialpadNumericChar(ch);
+  }
 
-  /*
-   * Returns true if the provided character is a letter, and can be mapped to a key on the dialpad
+  /**
+   * Returns true if the provided character is a letter and can be mapped to a key on the dialpad.
+   *
+   * <p>The provided character is expected to be a normalized character. See {@link
+   * SmartDialMap#normalizeCharacter(char)} for details.
    */
-  boolean isValidDialpadAlphabeticChar(char ch);
+  protected boolean isValidDialpadAlphabeticChar(char ch) {
+    return getCharToKeyMap().containsKey(ch);
+  }
 
-  /*
-   * Returns true if the provided character is a digit, and can be mapped to a key on the dialpad
+  /**
+   * Returns true if the provided character is a digit, and can be mapped to a key on the dialpad.
    */
-  boolean isValidDialpadNumericChar(char ch);
+  protected boolean isValidDialpadNumericChar(char ch) {
+    return '0' <= ch && ch <= '9';
+  }
 
-  /*
-   * Get the index of the key on the dialpad which the character corresponds to
+  /**
+   * Get the index of the key on the dialpad which the character corresponds to.
+   *
+   * <p>The provided character is expected to be a normalized character. See {@link
+   * SmartDialMap#normalizeCharacter(char)} for details.
+   *
+   * <p>An {@link Optional#absent()} is returned if the provided character can't be mapped to a key
+   * on the dialpad.
    */
-  byte getDialpadIndex(char ch);
+  protected Optional<Byte> getDialpadIndex(char ch) {
+    if (isValidDialpadNumericChar(ch)) {
+      return Optional.of((byte) (ch - '0'));
+    }
 
-  /*
-   * Get the actual numeric character on the dialpad which the character corresponds to
+    if (isValidDialpadAlphabeticChar(ch)) {
+      return Optional.of((byte) (getCharToKeyMap().get(ch) - '0'));
+    }
+
+    return Optional.absent();
+  }
+
+  /**
+   * Get the actual numeric character on the dialpad which the character corresponds to.
+   *
+   * <p>The provided character is expected to be a normalized character. See {@link
+   * SmartDialMap#normalizeCharacter(char)} for details.
+   *
+   * <p>An {@link Optional#absent()} is returned if the provided character can't be mapped to a key
+   * on the dialpad.
    */
-  char getDialpadNumericCharacter(char ch);
+  protected Optional<Character> getDialpadNumericCharacter(char ch) {
+    return isValidDialpadAlphabeticChar(ch)
+        ? Optional.of(getCharToKeyMap().get(ch))
+        : Optional.absent();
+  }
 
-  /*
+  /**
    * Converts uppercase characters to lower case ones, and on a best effort basis, strips accents
    * from accented characters.
+   *
+   * <p>An {@link Optional#absent()} is returned if the provided character can't be mapped to a key
+   * on the dialpad.
    */
-  char normalizeCharacter(char ch);
+  abstract Optional<Character> normalizeCharacter(char ch);
+
+  /**
+   * Returns a map in which each key is a normalized character and the corresponding value is a
+   * dialpad key.
+   */
+  abstract SimpleArrayMap<Character, Character> getCharToKeyMap();
 }
diff --git a/java/com/android/dialer/smartdial/SmartDialNameMatcher.java b/java/com/android/dialer/smartdial/SmartDialNameMatcher.java
index a9256ec..4e3e0cc 100644
--- a/java/com/android/dialer/smartdial/SmartDialNameMatcher.java
+++ b/java/com/android/dialer/smartdial/SmartDialNameMatcher.java
@@ -16,6 +16,7 @@
 
 package com.android.dialer.smartdial;
 
+import android.content.Context;
 import android.support.annotation.Nullable;
 import android.text.TextUtils;
 import com.android.dialer.smartdial.SmartDialPrefix.PhoneNumberTokens;
@@ -29,8 +30,6 @@
  * (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;
 
@@ -39,15 +38,13 @@
   private static final int INITIAL_LENGTH_LIMIT = 1;
 
   private final ArrayList<SmartDialMatchPosition> mMatchPositions = new ArrayList<>();
-  private final SmartDialMap mMap;
   private String mQuery;
 
   // Controls whether to treat an empty query as a match (with anything).
   private boolean mShouldMatchEmptyQuery = false;
 
-  public SmartDialNameMatcher(String query, SmartDialMap map) {
+  public SmartDialNameMatcher(String query) {
     mQuery = query;
-    mMap = map;
   }
 
   /**
@@ -56,8 +53,8 @@
    * @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);
+  public static String normalizeNumber(Context context, String number) {
+    return normalizeNumber(context, number, /* offset = */ 0);
   }
 
   /**
@@ -67,11 +64,11 @@
    * @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) {
+  public static String normalizeNumber(Context context, String number, int offset) {
     final StringBuilder s = new StringBuilder();
     for (int i = offset; i < number.length(); i++) {
       char ch = number.charAt(i);
-      if (map.isValidDialpadNumericChar(ch)) {
+      if (CompositeSmartDialMap.isValidDialpadNumericChar(context, ch)) {
         s.append(ch);
       }
     }
@@ -112,7 +109,7 @@
    *     with the matching positions otherwise
    */
   @Nullable
-  public SmartDialMatchPosition matchesNumber(String phoneNumber, String query) {
+  public SmartDialMatchPosition matchesNumber(Context context, String phoneNumber, String query) {
     if (TextUtils.isEmpty(phoneNumber)) {
       return mShouldMatchEmptyQuery ? new SmartDialMatchPosition(0, 0) : null;
     }
@@ -120,15 +117,19 @@
     constructEmptyMask(builder, phoneNumber.length());
 
     // Try matching the number as is
-    SmartDialMatchPosition matchPos = matchesNumberWithOffset(phoneNumber, query, 0);
+    SmartDialMatchPosition matchPos =
+        matchesNumberWithOffset(context, phoneNumber, query, /* offset = */ 0);
     if (matchPos == null) {
-      PhoneNumberTokens phoneNumberTokens = SmartDialPrefix.parsePhoneNumber(phoneNumber);
+      PhoneNumberTokens phoneNumberTokens = SmartDialPrefix.parsePhoneNumber(context, phoneNumber);
 
       if (phoneNumberTokens.countryCodeOffset != 0) {
-        matchPos = matchesNumberWithOffset(phoneNumber, query, phoneNumberTokens.countryCodeOffset);
+        matchPos =
+            matchesNumberWithOffset(
+                context, phoneNumber, query, phoneNumberTokens.countryCodeOffset);
       }
       if (matchPos == null && phoneNumberTokens.nanpCodeOffset != 0) {
-        matchPos = matchesNumberWithOffset(phoneNumber, query, phoneNumberTokens.nanpCodeOffset);
+        matchPos =
+            matchesNumberWithOffset(context, phoneNumber, query, phoneNumberTokens.nanpCodeOffset);
       }
     }
     if (matchPos != null) {
@@ -145,8 +146,8 @@
    * @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);
+  public SmartDialMatchPosition matchesNumber(Context context, String phoneNumber) {
+    return matchesNumber(context, phoneNumber, mQuery);
   }
 
   /**
@@ -160,7 +161,7 @@
    *     with the matching positions otherwise
    */
   private SmartDialMatchPosition matchesNumberWithOffset(
-      String phoneNumber, String query, int offset) {
+      Context context, String phoneNumber, String query, int offset) {
     if (TextUtils.isEmpty(phoneNumber) || TextUtils.isEmpty(query)) {
       return mShouldMatchEmptyQuery ? new SmartDialMatchPosition(offset, offset) : null;
     }
@@ -171,7 +172,7 @@
         break;
       }
       char ch = phoneNumber.charAt(i);
-      if (mMap.isValidDialpadNumericChar(ch)) {
+      if (CompositeSmartDialMap.isValidDialpadNumericChar(context, ch)) {
         if (ch != query.charAt(queryAt)) {
           return null;
         }
@@ -225,7 +226,10 @@
    *     match positions (multiple matches correspond to initial matches).
    */
   private boolean matchesCombination(
-      String displayName, String query, ArrayList<SmartDialMatchPosition> matchList) {
+      Context context,
+      String displayName,
+      String query,
+      ArrayList<SmartDialMatchPosition> matchList) {
     StringBuilder builder = new StringBuilder();
     constructEmptyMask(builder, displayName.length());
     final int nameLength = displayName.length();
@@ -260,10 +264,10 @@
     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);
+      ch = CompositeSmartDialMap.normalizeCharacter(context, ch);
+      if (CompositeSmartDialMap.isValidDialpadCharacter(context, ch)) {
+        if (CompositeSmartDialMap.isValidDialpadAlphabeticChar(context, ch)) {
+          ch = CompositeSmartDialMap.getDialpadNumericCharacter(context, ch);
         }
         if (ch != query.charAt(queryStart)) {
           // Failed to match the current character in the query.
@@ -283,12 +287,16 @@
           // then skip to the end of the "Yoghurt" token.
 
           if (queryStart == 0
-              || mMap.isValidDialpadCharacter(
-                  mMap.normalizeCharacter(displayName.charAt(nameStart - 1)))) {
+              || CompositeSmartDialMap.isValidDialpadCharacter(
+                  context,
+                  CompositeSmartDialMap.normalizeCharacter(
+                      context, 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)))) {
+                && CompositeSmartDialMap.isValidDialpadCharacter(
+                    context,
+                    CompositeSmartDialMap.normalizeCharacter(
+                        context, displayName.charAt(nameStart)))) {
               nameStart++;
             }
             nameStart++;
@@ -316,7 +324,9 @@
             // find the next separator in the query string
             int j;
             for (j = nameStart; j < nameLength; j++) {
-              if (!mMap.isValidDialpadCharacter(mMap.normalizeCharacter(displayName.charAt(j)))) {
+              if (!CompositeSmartDialMap.isValidDialpadCharacter(
+                  context,
+                  CompositeSmartDialMap.normalizeCharacter(context, displayName.charAt(j)))) {
                 break;
               }
             }
@@ -324,7 +334,8 @@
             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)) {
+              if (matchesCombination(
+                  context, remainder, query.substring(queryStart + 1), partialTemp)) {
 
                 // store the list of possible match positions
                 SmartDialMatchPosition.advanceMatchPositions(partialTemp, j + 1);
@@ -393,9 +404,9 @@
    *     contained in query. If the function returns true, matchList will contain an ArrayList of
    *     match positions (multiple matches correspond to initial matches).
    */
-  public boolean matches(String displayName) {
+  public boolean matches(Context context, String displayName) {
     mMatchPositions.clear();
-    return matchesCombination(displayName, mQuery, mMatchPositions);
+    return matchesCombination(context, displayName, mQuery, mMatchPositions);
   }
 
   public ArrayList<SmartDialMatchPosition> getMatchPositions() {
diff --git a/java/com/android/dialer/smartdial/SmartDialPrefix.java b/java/com/android/dialer/smartdial/SmartDialPrefix.java
index 36f174b..b9c1f8c 100644
--- a/java/com/android/dialer/smartdial/SmartDialPrefix.java
+++ b/java/com/android/dialer/smartdial/SmartDialPrefix.java
@@ -52,8 +52,6 @@
       "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. */
@@ -95,7 +93,7 @@
    * @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) {
+  public static ArrayList<String> parseToIndexTokens(Context context, String contactName) {
     final int length = contactName.length();
     final ArrayList<String> result = new ArrayList<>();
     char c;
@@ -106,10 +104,10 @@
      * " ", 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)) {
+      c = CompositeSmartDialMap.normalizeCharacter(context, contactName.charAt(i));
+      if (CompositeSmartDialMap.isValidDialpadCharacter(context, c)) {
         /** Converts a character into the number on dialpad that represents the character. */
-        currentIndexToken.append(mMap.getDialpadIndex(c));
+        currentIndexToken.append(CompositeSmartDialMap.getDialpadIndex(context, c));
       } else {
         if (currentIndexToken.length() != 0) {
           result.add(currentIndexToken.toString());
@@ -132,11 +130,11 @@
    * @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) {
+  public static ArrayList<String> generateNamePrefixes(Context context, String index) {
     final ArrayList<String> result = new ArrayList<>();
 
     /** Parses the name into a list of tokens. */
-    final ArrayList<String> indexTokens = parseToIndexTokens(index);
+    final ArrayList<String> indexTokens = parseToIndexTokens(context, index);
 
     if (indexTokens.size() > 0) {
       /**
@@ -198,13 +196,13 @@
    * @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) {
+  public static ArrayList<String> parseToNumberTokens(Context context, 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));
+      result.add(SmartDialNameMatcher.normalizeNumber(context, number));
 
-      final PhoneNumberTokens phoneNumberTokens = parsePhoneNumber(number);
+      final PhoneNumberTokens phoneNumberTokens = parsePhoneNumber(context, number);
       if (phoneNumberTokens == null) {
         return result;
       }
@@ -212,12 +210,13 @@
       if (phoneNumberTokens.countryCodeOffset != 0) {
         result.add(
             SmartDialNameMatcher.normalizeNumber(
-                number, phoneNumberTokens.countryCodeOffset, mMap));
+                context, number, phoneNumberTokens.countryCodeOffset));
       }
 
       if (phoneNumberTokens.nanpCodeOffset != 0) {
         result.add(
-            SmartDialNameMatcher.normalizeNumber(number, phoneNumberTokens.nanpCodeOffset, mMap));
+            SmartDialNameMatcher.normalizeNumber(
+                context, number, phoneNumberTokens.nanpCodeOffset));
       }
     }
     return result;
@@ -229,13 +228,13 @@
    * @param number Raw phone number.
    * @return a PhoneNumberToken instance with country code, NANP code information.
    */
-  public static PhoneNumberTokens parsePhoneNumber(String number) {
+  public static PhoneNumberTokens parsePhoneNumber(Context context, String number) {
     String countryCode = "";
     int countryCodeOffset = 0;
     int nanpNumberOffset = 0;
 
     if (!TextUtils.isEmpty(number)) {
-      String normalizedNumber = SmartDialNameMatcher.normalizeNumber(number, mMap);
+      String normalizedNumber = SmartDialNameMatcher.normalizeNumber(context, number);
       if (number.charAt(0) == '+') {
         /** If the number starts with '+', tries to find valid country code. */
         for (int i = 1; i <= 1 + 3; i++) {
@@ -518,10 +517,6 @@
     return result;
   }
 
-  public static SmartDialMap getMap() {
-    return mMap;
-  }
-
   /**
    * Indicates whether the given country uses NANP numbers
    *