Merge changes I0115fb26,I7a29bf5a,I7888f3b6,I06d5d3d8,I4c601b16, ...

* changes:
  Implemented CompositePhoneLookup#bulkUpdate.
  Allow the client to specify voicemail transcript id's
  Implement bulk update for Cp2PhoneLookup.
  Add character mappings for Bulgarian & Ukrainian in DialpadCharMappings.
  Support dual alphabets in smart search when a secondary alphabet is available.
  Commit transaction synchronously for OnHoldFragment.
diff --git a/java/com/android/dialer/dialpadview/DialpadAlphabets.java b/java/com/android/dialer/dialpadview/DialpadAlphabets.java
deleted file mode 100644
index f02ca43..0000000
--- a/java/com/android/dialer/dialpadview/DialpadAlphabets.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * 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.dialpadview;
-
-import android.support.v4.util.SimpleArrayMap;
-
-/** A class containing key-letter mappings for the dialpad. */
-public class DialpadAlphabets {
-
-  // The default mapping (the Latin alphabet)
-  private static final String[] def = {
-    "+" /* 0 */,
-    "" /* 1 */,
-    "ABC" /* 2 */,
-    "DEF" /* 3 */,
-    "GHI" /* 4 */,
-    "JKL" /* 5 */,
-    "MNO" /* 6 */,
-    "PQRS" /* 7 */,
-    "TUV" /* 8 */,
-    "WXYZ" /* 9 */,
-    "" /* * */,
-    "" /* # */,
-  };
-
-  // Russian
-  private static final String[] rus = {
-    "" /* 0 */,
-    "" /* 1 */,
-    "АБВГ" /* 2 */,
-    "ДЕЖЗ" /* 3 */,
-    "ИЙКЛ" /* 4 */,
-    "МНОП" /* 5 */,
-    "РСТУ" /* 6 */,
-    "ФХЦЧ" /* 7 */,
-    "ШЩЪЫ" /* 8 */,
-    "ЬЭЮЯ" /* 9 */,
-    "" /* * */,
-    "" /* # */,
-  };
-
-  // A map in which each key is an ISO 639-2 language code and the corresponding key is an array
-  // defining key-letter mappings
-  private static final SimpleArrayMap<String, String[]> alphabets = new SimpleArrayMap<>();
-
-  static {
-    alphabets.put("rus", rus);
-  }
-
-  /**
-   * Returns the alphabet (a key-letter mapping) of the given ISO 639-2 language code or null if
-   *
-   * <ul>
-   *   <li>no alphabet for the language code is defined, or
-   *   <li>the language code is invalid.
-   * </ul>
-   */
-  public static String[] getAlphabetForLanguage(String languageCode) {
-    return alphabets.get(languageCode);
-  }
-
-  /** Returns the default key-letter mapping (the one that uses the Latin alphabet). */
-  public static String[] getDefaultAlphabet() {
-    return def;
-  }
-}
diff --git a/java/com/android/dialer/dialpadview/DialpadCharMappings.java b/java/com/android/dialer/dialpadview/DialpadCharMappings.java
new file mode 100644
index 0000000..03bc2e7
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/DialpadCharMappings.java
@@ -0,0 +1,228 @@
+/*
+ * 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.dialpadview;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.util.SimpleArrayMap;
+import com.android.dialer.common.Assert;
+import com.android.dialer.compat.CompatUtils;
+import com.android.dialer.configprovider.ConfigProviderBindings;
+
+/** A class containing character mappings for the dialpad. */
+public class DialpadCharMappings {
+  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+  public static final String FLAG_ENABLE_DUAL_ALPHABETS = "enable_dual_alphabets_on_t9";
+
+  /** The character mapping for the Latin alphabet (the default mapping) */
+  private static class Latin {
+    private static final String[] KEY_TO_CHARS = {
+      "+" /* 0 */,
+      "" /* 1 */,
+      "ABC" /* 2 */,
+      "DEF" /* 3 */,
+      "GHI" /* 4 */,
+      "JKL" /* 5 */,
+      "MNO" /* 6 */,
+      "PQRS" /* 7 */,
+      "TUV" /* 8 */,
+      "WXYZ" /* 9 */,
+      "" /* * */,
+      "" /* # */,
+    };
+
+    private static final SimpleArrayMap<Character, Character> CHAR_TO_KEY =
+        getCharToKeyMap(KEY_TO_CHARS);
+  }
+
+  /** The character mapping for the Bulgarian alphabet */
+  private static class Bul {
+    private static final String[] KEY_TO_CHARS = {
+      "" /* 0 */,
+      "" /* 1 */,
+      "АБВГ" /* 2 */,
+      "ДЕЖЗ" /* 3 */,
+      "ИЙКЛ" /* 4 */,
+      "МНО" /* 5 */,
+      "ПРС" /* 6 */,
+      "ТУФХ" /* 7 */,
+      "ЦЧШЩ" /* 8 */,
+      "ЪЬЮЯ" /* 9 */,
+      "" /* * */,
+      "" /* # */,
+    };
+
+    private static final SimpleArrayMap<Character, Character> CHAR_TO_KEY =
+        getCharToKeyMap(KEY_TO_CHARS);
+  }
+
+  /** The character mapping for the Russian alphabet */
+  private static class Rus {
+    private static final String[] KEY_TO_CHARS = {
+      "" /* 0 */,
+      "" /* 1 */,
+      "АБВГ" /* 2 */,
+      "ДЕЁЖЗ" /* 3 */,
+      "ИЙКЛ" /* 4 */,
+      "МНОП" /* 5 */,
+      "РСТУ" /* 6 */,
+      "ФХЦЧ" /* 7 */,
+      "ШЩЪЫ" /* 8 */,
+      "ЬЭЮЯ" /* 9 */,
+      "" /* * */,
+      "" /* # */,
+    };
+
+    private static final SimpleArrayMap<Character, Character> CHAR_TO_KEY =
+        getCharToKeyMap(KEY_TO_CHARS);
+  }
+
+  /** The character mapping for the Ukrainian alphabet */
+  private static class Ukr {
+    private static final String[] KEY_TO_CHARS = {
+      "" /* 0 */,
+      "" /* 1 */,
+      "АБВГҐ" /* 2 */,
+      "ДЕЄЖЗ" /* 3 */,
+      "ИІЇЙКЛ" /* 4 */,
+      "МНОП" /* 5 */,
+      "РСТУ" /* 6 */,
+      "ФХЦЧ" /* 7 */,
+      "ШЩ" /* 8 */,
+      "ЬЮЯ" /* 9 */,
+      "" /* * */,
+      "" /* # */,
+    };
+
+    private static final SimpleArrayMap<Character, Character> CHAR_TO_KEY =
+        getCharToKeyMap(KEY_TO_CHARS);
+  }
+
+  // A map in which each key is an ISO 639-2 language code and the corresponding value is a
+  // character-key map.
+  private static final SimpleArrayMap<String, SimpleArrayMap<Character, Character>>
+      CHAR_TO_KEY_MAPS = new SimpleArrayMap<>();
+
+  // A map in which each key is an ISO 639-2 language code and the corresponding value is an array
+  // defining a key-characters map.
+  private static final SimpleArrayMap<String, String[]> KEY_TO_CHAR_MAPS = new SimpleArrayMap<>();
+
+  static {
+    CHAR_TO_KEY_MAPS.put("bul", Bul.CHAR_TO_KEY);
+    CHAR_TO_KEY_MAPS.put("rus", Rus.CHAR_TO_KEY);
+    CHAR_TO_KEY_MAPS.put("ukr", Ukr.CHAR_TO_KEY);
+
+    KEY_TO_CHAR_MAPS.put("bul", Bul.KEY_TO_CHARS);
+    KEY_TO_CHAR_MAPS.put("rus", Rus.KEY_TO_CHARS);
+    KEY_TO_CHAR_MAPS.put("ukr", Ukr.KEY_TO_CHARS);
+  }
+
+  /**
+   * Returns the character-key map of the ISO 639-2 language code of the 1st language preference or
+   * null if
+   *
+   * <ul>
+   *   <li>no character-key map for the language code is defined, or
+   *   <li>the support for dual alphabets is disabled.
+   * </ul>
+   */
+  public static SimpleArrayMap<Character, Character> getCharToKeyMap(@NonNull Context context) {
+    return isDualAlphabetsEnabled(context)
+        ? CHAR_TO_KEY_MAPS.get(CompatUtils.getLocale(context).getISO3Language())
+        : null;
+  }
+
+  /** Returns the default character-key map (the one that uses the Latin alphabet). */
+  public static SimpleArrayMap<Character, Character> getDefaultCharToKeyMap() {
+    return Latin.CHAR_TO_KEY;
+  }
+
+  /**
+   * Returns the key-characters map of the given ISO 639-2 language code of the 1st language
+   * preference or null if
+   *
+   * <ul>
+   *   <li>no key-characters map for the language code is defined, or
+   *   <li>the support for dual alphabets is disabled.
+   * </ul>
+   */
+  public static String[] getKeyToCharsMap(@NonNull Context context) {
+    return isDualAlphabetsEnabled(context)
+        ? KEY_TO_CHAR_MAPS.get(CompatUtils.getLocale(context).getISO3Language())
+        : null;
+  }
+
+  /** Returns the default key-characters map (the one that uses the Latin alphabet). */
+  public static String[] getDefaultKeyToCharsMap() {
+    return Latin.KEY_TO_CHARS;
+  }
+
+  private static boolean isDualAlphabetsEnabled(Context context) {
+    return ConfigProviderBindings.get(context).getBoolean(FLAG_ENABLE_DUAL_ALPHABETS, false);
+  }
+
+  /**
+   * Given a array representing a key-characters map, return its reverse map.
+   *
+   * <p>It is the caller's responsibility to ensure that
+   *
+   * <ul>
+   *   <li>the array contains only 12 elements,
+   *   <li>the 0th element ~ the 9th element are the mappings for keys "0" ~ "9",
+   *   <li>the 10th element is for key "*", and
+   *   <li>the 11th element is for key "#".
+   * </ul>
+   *
+   * @param keyToChars An array representing a key-characters map. It must satisfy the conditions
+   *     above.
+   * @return A character-key map.
+   */
+  private static SimpleArrayMap<Character, Character> getCharToKeyMap(
+      @NonNull String[] keyToChars) {
+    Assert.checkArgument(keyToChars.length == 12);
+
+    SimpleArrayMap<Character, Character> charToKeyMap = new SimpleArrayMap<>();
+
+    for (int keyIndex = 0; keyIndex < keyToChars.length; keyIndex++) {
+      String chars = keyToChars[keyIndex];
+
+      for (int j = 0; j < chars.length(); j++) {
+        char c = chars.charAt(j);
+        if (Character.isAlphabetic(c)) {
+          charToKeyMap.put(Character.toLowerCase(c), getKeyChar(keyIndex));
+        }
+      }
+    }
+
+    return charToKeyMap;
+  }
+
+  /** Given a key index of the dialpad, returns the corresponding character. */
+  private static char getKeyChar(int keyIndex) {
+    Assert.checkArgument(0 <= keyIndex && keyIndex <= 11);
+
+    switch (keyIndex) {
+      case 10:
+        return '*';
+      case 11:
+        return '#';
+      default:
+        return (char) ('0' + keyIndex);
+    }
+  }
+}
diff --git a/java/com/android/dialer/dialpadview/DialpadView.java b/java/com/android/dialer/dialpadview/DialpadView.java
index 38ab383..5794038 100644
--- a/java/com/android/dialer/dialpadview/DialpadView.java
+++ b/java/com/android/dialer/dialpadview/DialpadView.java
@@ -114,9 +114,8 @@
     mIsRtl =
         TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL;
 
-    mPrimaryLettersMapping = DialpadAlphabets.getDefaultAlphabet();
-    mSecondaryLettersMapping =
-        DialpadAlphabets.getAlphabetForLanguage(CompatUtils.getLocale(context).getISO3Language());
+    mPrimaryLettersMapping = DialpadCharMappings.getDefaultKeyToCharsMap();
+    mSecondaryLettersMapping = DialpadCharMappings.getKeyToCharsMap(context);
   }
 
   @Override
diff --git a/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java b/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java
index ba08fe9..f85b357 100644
--- a/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java
+++ b/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java
@@ -19,6 +19,7 @@
 import android.support.annotation.NonNull;
 import android.telecom.Call;
 import com.android.dialer.DialerPhoneNumber;
+import com.android.dialer.common.LogUtil;
 import com.android.dialer.common.concurrent.DialerFutures;
 import com.android.dialer.phonelookup.PhoneLookup;
 import com.android.dialer.phonelookup.PhoneLookupInfo;
@@ -85,9 +86,47 @@
         futures, Preconditions::checkNotNull, false /* defaultValue */);
   }
 
+  /**
+   * Delegates to a set of dependent lookups and combines results.
+   *
+   * <p>Note: If any of the dependent lookups fails, the returned future will also fail. If any of
+   * the dependent lookups does not complete, the returned future will also not complete.
+   */
   @Override
   public ListenableFuture<ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>> bulkUpdate(
       ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> existingInfoMap, long lastModified) {
-    return null;
+    List<ListenableFuture<ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>>> futures =
+        new ArrayList<>();
+    for (PhoneLookup phoneLookup : phoneLookups) {
+      futures.add(phoneLookup.bulkUpdate(existingInfoMap, lastModified));
+    }
+    return Futures.transform(
+        Futures.allAsList(futures),
+        new Function<
+            List<ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>>,
+            ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>>() {
+          @Override
+          public ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> apply(
+              List<ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>> allMaps) {
+            ImmutableMap.Builder<DialerPhoneNumber, PhoneLookupInfo> combinedMap =
+                ImmutableMap.builder();
+            for (DialerPhoneNumber dialerPhoneNumber : existingInfoMap.keySet()) {
+              PhoneLookupInfo.Builder combinedInfo = PhoneLookupInfo.newBuilder();
+              for (ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> map : allMaps) {
+                PhoneLookupInfo subInfo = map.get(dialerPhoneNumber);
+                if (subInfo == null) {
+                  throw new IllegalStateException(
+                      "A sublookup didn't return an info for number: "
+                          + LogUtil.sanitizePhoneNumber(
+                              dialerPhoneNumber.getRawInput().getNumber()));
+                }
+                combinedInfo.mergeFrom(subInfo);
+              }
+              combinedMap.put(dialerPhoneNumber, combinedInfo.build());
+            }
+            return combinedMap.build();
+          }
+        },
+        MoreExecutors.directExecutor());
   }
 }
diff --git a/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java b/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java
index f9fc1a6..2878e27 100644
--- a/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java
+++ b/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java
@@ -22,23 +22,47 @@
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.DeletedContacts;
 import android.support.annotation.NonNull;
+import android.support.v4.util.ArrayMap;
 import android.support.v4.util.ArraySet;
 import android.telecom.Call;
+import android.text.TextUtils;
 import com.android.dialer.DialerPhoneNumber;
+import com.android.dialer.common.Assert;
 import com.android.dialer.common.concurrent.DialerExecutors;
 import com.android.dialer.inject.ApplicationContext;
 import com.android.dialer.phonelookup.PhoneLookup;
 import com.android.dialer.phonelookup.PhoneLookupInfo;
+import com.android.dialer.phonelookup.PhoneLookupInfo.Cp2Info;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.MoreExecutors;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
 import java.util.Set;
 import javax.inject.Inject;
 
 /** PhoneLookup implementation for local contacts. */
 public final class Cp2PhoneLookup implements PhoneLookup {
 
+  private static final String[] CP2_INFO_PROJECTION =
+      new String[] {
+        Phone.DISPLAY_NAME_PRIMARY, // 0
+        Phone.PHOTO_THUMBNAIL_URI, // 1
+        Phone.PHOTO_ID, // 2
+        Phone.LABEL, // 3
+        Phone.NORMALIZED_NUMBER, // 4
+        Phone.CONTACT_ID, // 5
+      };
+
+  private static final int CP2_INFO_NAME_INDEX = 0;
+  private static final int CP2_INFO_PHOTO_URI_INDEX = 1;
+  private static final int CP2_INFO_PHOTO_ID_INDEX = 2;
+  private static final int CP2_INFO_LABEL_INDEX = 3;
+  private static final int CP2_INFO_NUMBER_INDEX = 4;
+  private static final int CP2_INFO_CONTACT_ID_INDEX = 5;
+
   private final Context appContext;
 
   @Inject
@@ -60,12 +84,12 @@
   }
 
   private boolean isDirtyInternal(ImmutableSet<DialerPhoneNumber> phoneNumbers, long lastModified) {
-    return contactsUpdated(getContactIdsFromPhoneNumbers(phoneNumbers), lastModified)
+    return contactsUpdated(queryPhoneTableForContactIds(phoneNumbers), lastModified)
         || contactsDeleted(lastModified);
   }
 
   /** Returns set of contact ids that correspond to {@code phoneNumbers} if the contact exists. */
-  private Set<Long> getContactIdsFromPhoneNumbers(ImmutableSet<DialerPhoneNumber> phoneNumbers) {
+  private Set<Long> queryPhoneTableForContactIds(ImmutableSet<DialerPhoneNumber> phoneNumbers) {
     Set<Long> contactIds = new ArraySet<>();
     try (Cursor cursor =
         appContext
@@ -73,7 +97,7 @@
             .query(
                 Phone.CONTENT_URI,
                 new String[] {Phone.CONTACT_ID},
-                columnInSetWhereStatement(Phone.NORMALIZED_NUMBER, phoneNumbers.size()),
+                Phone.NORMALIZED_NUMBER + " IN (" + questionMarks(phoneNumbers.size()) + ")",
                 contactIdsSelectionArgs(phoneNumbers),
                 null)) {
       cursor.moveToPosition(-1);
@@ -100,37 +124,32 @@
 
   /** Returns true if any contacts were modified after {@code lastModified}. */
   private boolean contactsUpdated(Set<Long> contactIds, long lastModified) {
-    try (Cursor cursor =
-        appContext
-            .getContentResolver()
-            .query(
-                Contacts.CONTENT_URI,
-                new String[] {Contacts._ID},
-                contactsIsDirtyWhereStatement(contactIds.size()),
-                contactsIsDirtySelectionArgs(lastModified, contactIds),
-                null)) {
+    try (Cursor cursor = queryContactsTableForContacts(contactIds, lastModified)) {
       return cursor.getCount() > 0;
     }
   }
 
-  private static String contactsIsDirtyWhereStatement(int numberOfContactIds) {
-    StringBuilder where = new StringBuilder();
-    // Filter to after last modified time
-    where.append(Contacts.CONTACT_LAST_UPDATED_TIMESTAMP).append(" > ?");
+  private Cursor queryContactsTableForContacts(Set<Long> contactIds, long lastModified) {
+    // Filter to after last modified time based only on contacts we care about
+    String where =
+        Contacts.CONTACT_LAST_UPDATED_TIMESTAMP
+            + " > ?"
+            + " AND "
+            + Contacts._ID
+            + " IN ("
+            + questionMarks(contactIds.size())
+            + ")";
 
-    // Filter based only on contacts we care about
-    where.append(" AND ").append(columnInSetWhereStatement(Contacts._ID, numberOfContactIds));
-    return where.toString();
-  }
-
-  private String[] contactsIsDirtySelectionArgs(long lastModified, Set<Long> contactIds) {
     String[] args = new String[contactIds.size() + 1];
     args[0] = Long.toString(lastModified);
     int i = 1;
     for (Long contactId : contactIds) {
       args[i++] = Long.toString(contactId);
     }
-    return args;
+
+    return appContext
+        .getContentResolver()
+        .query(Contacts.CONTENT_URI, new String[] {Contacts._ID}, where, args, null);
   }
 
   /** Returns true if any contacts were deleted after {@code lastModified}. */
@@ -148,22 +167,272 @@
     }
   }
 
-  private static String columnInSetWhereStatement(String columnName, int setSize) {
+  @Override
+  public ListenableFuture<ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>> bulkUpdate(
+      ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> existingInfoMap, long lastModified) {
+    return MoreExecutors.listeningDecorator(DialerExecutors.getLowPriorityThreadPool(appContext))
+        .submit(() -> bulkUpdateInternal(existingInfoMap, lastModified));
+  }
+
+  private ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> bulkUpdateInternal(
+      ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> existingInfoMap, long lastModified) {
+    // Build a set of each DialerPhoneNumber that was associated with a contact, and is no longer
+    // associated with that same contact.
+    Set<DialerPhoneNumber> deletedPhoneNumbers =
+        getDeletedPhoneNumbers(existingInfoMap, lastModified);
+
+    // For each DialerPhoneNumber that was associated with a contact or added to a contact,
+    // build a map of those DialerPhoneNumbers to a set Cp2Infos, where each Cp2Info represents a
+    // contact.
+    ImmutableMap<DialerPhoneNumber, Set<Cp2Info>> updatedContacts =
+        buildMapForUpdatedOrAddedContacts(existingInfoMap, lastModified, deletedPhoneNumbers);
+
+    // Start build a new map of updated info. This will replace existing info.
+    ImmutableMap.Builder<DialerPhoneNumber, PhoneLookupInfo> newInfoMapBuilder =
+        ImmutableMap.builder();
+
+    // For each DialerPhoneNumber in existing info...
+    for (Entry<DialerPhoneNumber, PhoneLookupInfo> entry : existingInfoMap.entrySet()) {
+      // Build off the existing info
+      PhoneLookupInfo.Builder infoBuilder = PhoneLookupInfo.newBuilder(entry.getValue());
+
+      // If the contact was updated, replace the Cp2Info list
+      if (updatedContacts.containsKey(entry.getKey())) {
+        infoBuilder.clearCp2Info();
+        infoBuilder.addAllCp2Info(updatedContacts.get(entry.getKey()));
+
+        // If it was deleted and not added to a new contact, replace the Cp2Info list with
+        // the default instance of Cp2Info
+      } else if (deletedPhoneNumbers.contains(entry.getKey())) {
+        infoBuilder.clearCp2Info();
+        infoBuilder.addCp2Info(Cp2Info.getDefaultInstance());
+      }
+
+      // If the DialerPhoneNumber didn't change, add the unchanged existing info.
+      newInfoMapBuilder.put(entry.getKey(), infoBuilder.build());
+    }
+    return newInfoMapBuilder.build();
+  }
+
+  /**
+   * 1. get all contact ids. if the id is unset, add the number to the list of contacts to look up.
+   * 2. reduce our list of contact ids to those that were updated after lastModified. 3. Now we have
+   * the smallest set of dialer phone numbers to query cp2 against. 4. build and return the map of
+   * dialerphonenumbers to their new cp2info
+   *
+   * @return Map of {@link DialerPhoneNumber} to {@link PhoneLookupInfo} with updated {@link
+   *     Cp2Info}.
+   */
+  private ImmutableMap<DialerPhoneNumber, Set<Cp2Info>> buildMapForUpdatedOrAddedContacts(
+      ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> existingInfoMap,
+      long lastModified,
+      Set<DialerPhoneNumber> deletedPhoneNumbers) {
+
+    // Start building a set of DialerPhoneNumbers that we want to update.
+    Set<DialerPhoneNumber> updatedNumbers = new ArraySet<>();
+
+    Set<Long> contactIds = new ArraySet<>();
+    for (Entry<DialerPhoneNumber, PhoneLookupInfo> entry : existingInfoMap.entrySet()) {
+      // If the number was deleted, we need to check if it was added to a new contact.
+      if (deletedPhoneNumbers.contains(entry.getKey())) {
+        updatedNumbers.add(entry.getKey());
+        continue;
+      }
+
+      // For each Cp2Info for each existing DialerPhoneNumber...
+      // Store the contact id if it exist, else automatically add the DialerPhoneNumber to our
+      // set of DialerPhoneNumbers we want to update.
+      for (Cp2Info cp2Info : entry.getValue().getCp2InfoList()) {
+        if (Objects.equals(cp2Info, Cp2Info.getDefaultInstance())) {
+          // If the number doesn't have any Cp2Info set to it, for various reasons, we need to look
+          // up the number to check if any exists.
+          // The various reasons this might happen are:
+          //  - An existing contact that wasn't in the call log is now in the call log.
+          //  - A number was in the call log before but has now been added to a contact.
+          //  - A number is in the call log, but isn't associated with any contact.
+          updatedNumbers.add(entry.getKey());
+        } else {
+          contactIds.add(cp2Info.getContactId());
+        }
+      }
+    }
+
+    // Query the contacts table and get those that whose Contacts.CONTACT_LAST_UPDATED_TIMESTAMP is
+    // after lastModified, such that Contacts._ID is in our set of contact IDs we build above.
+    try (Cursor cursor = queryContactsTableForContacts(contactIds, lastModified)) {
+      int contactIdIndex = cursor.getColumnIndex(Contacts._ID);
+      cursor.moveToPosition(-1);
+      while (cursor.moveToNext()) {
+        // Find the DialerPhoneNumber for each contact id and add it to our updated numbers set.
+        // These, along with our number not associated with any Cp2Info need to be updated.
+        long contactId = cursor.getLong(contactIdIndex);
+        updatedNumbers.addAll(getDialerPhoneNumber(existingInfoMap, contactId));
+      }
+    }
+
+    // Query the Phone table and build Cp2Info for each DialerPhoneNumber in our updatedNumbers set.
+    Map<DialerPhoneNumber, Set<Cp2Info>> map = new ArrayMap<>();
+    try (Cursor cursor = getAllCp2Rows(updatedNumbers)) {
+      cursor.moveToPosition(-1);
+      while (cursor.moveToNext()) {
+        // Map each dialer phone number to it's new cp2 info
+        Set<DialerPhoneNumber> phoneNumbers =
+            getDialerPhoneNumbers(updatedNumbers, cursor.getString(CP2_INFO_NUMBER_INDEX));
+        Cp2Info info = buildCp2InfoFromUpdatedContactsCursor(cursor);
+        for (DialerPhoneNumber phoneNumber : phoneNumbers) {
+          if (map.containsKey(phoneNumber)) {
+            map.get(phoneNumber).add(info);
+          } else {
+            Set<Cp2Info> cp2Infos = new ArraySet<>();
+            cp2Infos.add(info);
+            map.put(phoneNumber, cp2Infos);
+          }
+        }
+      }
+    }
+    return ImmutableMap.copyOf(map);
+  }
+
+  /**
+   * Returns cursor with projection {@link #CP2_INFO_PROJECTION} and only phone numbers that are in
+   * {@code updateNumbers}.
+   */
+  private Cursor getAllCp2Rows(Set<DialerPhoneNumber> updatedNumbers) {
+    String where = Phone.NORMALIZED_NUMBER + " IN (" + questionMarks(updatedNumbers.size()) + ")";
+    String[] selectionArgs = new String[updatedNumbers.size()];
+    int i = 0;
+    for (DialerPhoneNumber phoneNumber : updatedNumbers) {
+      selectionArgs[i++] = getNormalizedNumber(phoneNumber);
+    }
+
+    return appContext
+        .getContentResolver()
+        .query(Phone.CONTENT_URI, CP2_INFO_PROJECTION, where, selectionArgs, null);
+  }
+
+  /**
+   * @param cursor with projection {@link #CP2_INFO_PROJECTION}.
+   * @return new {@link Cp2Info} based on current row of {@code cursor}.
+   */
+  private static Cp2Info buildCp2InfoFromUpdatedContactsCursor(Cursor cursor) {
+    String displayName = cursor.getString(CP2_INFO_NAME_INDEX);
+    String photoUri = cursor.getString(CP2_INFO_PHOTO_URI_INDEX);
+    String label = cursor.getString(CP2_INFO_LABEL_INDEX);
+
+    Cp2Info.Builder infoBuilder = Cp2Info.newBuilder();
+    if (!TextUtils.isEmpty(displayName)) {
+      infoBuilder.setName(displayName);
+    }
+    if (!TextUtils.isEmpty(photoUri)) {
+      infoBuilder.setPhotoUri(photoUri);
+    }
+    if (!TextUtils.isEmpty(label)) {
+      infoBuilder.setLabel(label);
+    }
+    infoBuilder.setPhotoId(cursor.getLong(CP2_INFO_PHOTO_ID_INDEX));
+    infoBuilder.setContactId(cursor.getLong(CP2_INFO_CONTACT_ID_INDEX));
+    return infoBuilder.build();
+  }
+
+  /** Returns set of DialerPhoneNumbers that were associated with now deleted contacts. */
+  private Set<DialerPhoneNumber> getDeletedPhoneNumbers(
+      ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> existingInfoMap, long lastModified) {
+    // Build set of all contact IDs from our existing data. We're going to use this set to query
+    // against the DeletedContacts table and see if any of them were deleted.
+    Set<Long> contactIds = findContactIdsIn(existingInfoMap);
+
+    // Start building a set of DialerPhoneNumbers that were associated with now deleted contacts.
+    try (Cursor cursor = queryDeletedContacts(contactIds, lastModified)) {
+      // We now have a cursor/list of contact IDs that were associated with deleted contacts.
+      return findDeletedPhoneNumbersIn(existingInfoMap, cursor);
+    }
+  }
+
+  private Set<Long> findContactIdsIn(ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> map) {
+    Set<Long> contactIds = new ArraySet<>();
+    for (PhoneLookupInfo info : map.values()) {
+      for (Cp2Info cp2Info : info.getCp2InfoList()) {
+        contactIds.add(cp2Info.getContactId());
+      }
+    }
+    return contactIds;
+  }
+
+  private Cursor queryDeletedContacts(Set<Long> contactIds, long lastModified) {
+    String where =
+        DeletedContacts.CONTACT_DELETED_TIMESTAMP
+            + " > ?"
+            + " AND "
+            + DeletedContacts.CONTACT_ID
+            + " IN ("
+            + questionMarks(contactIds.size())
+            + ")";
+    String[] args = new String[contactIds.size() + 1];
+    args[0] = Long.toString(lastModified);
+    int i = 1;
+    for (Long contactId : contactIds) {
+      args[i++] = Long.toString(contactId);
+    }
+
+    return appContext
+        .getContentResolver()
+        .query(
+            DeletedContacts.CONTENT_URI,
+            new String[] {DeletedContacts.CONTACT_ID},
+            where,
+            args,
+            null);
+  }
+
+  /** Returns set of DialerPhoneNumbers that are associated with deleted contact IDs. */
+  private Set<DialerPhoneNumber> findDeletedPhoneNumbersIn(
+      ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> existingInfoMap, Cursor cursor) {
+    int contactIdIndex = cursor.getColumnIndexOrThrow(DeletedContacts.CONTACT_ID);
+    Set<DialerPhoneNumber> deletedPhoneNumbers = new ArraySet<>();
+    cursor.moveToPosition(-1);
+    while (cursor.moveToNext()) {
+      long contactId = cursor.getLong(contactIdIndex);
+      deletedPhoneNumbers.addAll(getDialerPhoneNumber(existingInfoMap, contactId));
+    }
+    return deletedPhoneNumbers;
+  }
+
+  private static Set<DialerPhoneNumber> getDialerPhoneNumbers(
+      Set<DialerPhoneNumber> phoneNumbers, String number) {
+    Set<DialerPhoneNumber> matches = new ArraySet<>();
+    for (DialerPhoneNumber phoneNumber : phoneNumbers) {
+      if (getNormalizedNumber(phoneNumber).equals(number)) {
+        matches.add(phoneNumber);
+      }
+    }
+    Assert.checkArgument(
+        matches.size() > 0, "Couldn't find DialerPhoneNumber for number: " + number);
+    return matches;
+  }
+
+  private static Set<DialerPhoneNumber> getDialerPhoneNumber(
+      ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> existingInfoMap, long contactId) {
+    Set<DialerPhoneNumber> matches = new ArraySet<>();
+    for (Entry<DialerPhoneNumber, PhoneLookupInfo> entry : existingInfoMap.entrySet()) {
+      for (Cp2Info cp2Info : entry.getValue().getCp2InfoList()) {
+        if (cp2Info.getContactId() == contactId) {
+          matches.add(entry.getKey());
+        }
+      }
+    }
+    Assert.checkArgument(
+        matches.size() > 0, "Couldn't find DialerPhoneNumber for contact ID: " + contactId);
+    return matches;
+  }
+
+  private static String questionMarks(int count) {
     StringBuilder where = new StringBuilder();
-    where.append(columnName).append(" IN (");
-    for (int i = 0; i < setSize; i++) {
+    for (int i = 0; i < count; i++) {
       if (i != 0) {
         where.append(", ");
       }
       where.append("?");
     }
-    return where.append(")").toString();
-  }
-
-  @Override
-  public ListenableFuture<ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>> bulkUpdate(
-      ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> existingInfoMap, long lastModified) {
-    // TODO(calderwoodra)
-    return null;
+    return where.toString();
   }
 }
diff --git a/java/com/android/dialer/phonelookup/phone_lookup_info.proto b/java/com/android/dialer/phonelookup/phone_lookup_info.proto
index 1027e5c..cb89a64 100644
--- a/java/com/android/dialer/phonelookup/phone_lookup_info.proto
+++ b/java/com/android/dialer/phonelookup/phone_lookup_info.proto
@@ -17,10 +17,23 @@
   // Information about a PhoneNumber retrieved from CP2. Cp2PhoneLookup is
   // responsible for populating the data in this message.
   message Cp2Info {
+    // android.provider.ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME_PRIMARY
     optional string name = 1;
+
+    // android.provider.ContactsContract.CommonDataKinds.Phone.PHOTO_THUMBNAIL_URI
     optional string photo_uri = 2;
+
+    // android.provider.ContactsContract.CommonDataKinds.Phone.PHOTO_ID
     optional fixed64 photo_id = 3;
-    optional string label = 4;  // "Home", "Mobile", ect.
+
+    // android.provider.ContactsContract.CommonDataKinds.Phone.LABEL
+    // "Home", "Mobile", ect.
+    optional string label = 4;
+
+    // android.provider.ContactsContract.CommonDataKinds.Phone.CONTACT_ID
+    optional fixed64 contact_id = 5;
   }
-  optional Cp2Info cp2_info = 1;
+  // Repeated because one phone number can be associated with multiple CP2
+  // contacts.
+  repeated Cp2Info cp2_info = 1;
 }
\ No newline at end of file
diff --git a/java/com/android/dialer/searchfragment/common/QueryBoldingUtil.java b/java/com/android/dialer/searchfragment/common/QueryBoldingUtil.java
index 4413252..9ac6e7c 100644
--- a/java/com/android/dialer/searchfragment/common/QueryBoldingUtil.java
+++ b/java/com/android/dialer/searchfragment/common/QueryBoldingUtil.java
@@ -16,6 +16,7 @@
 
 package com.android.dialer.searchfragment.common;
 
+import android.content.Context;
 import android.graphics.Typeface;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
@@ -49,14 +50,16 @@
    *
    * @param query containing any characters
    * @param name of a contact/string that query will compare to
+   * @param context of the app
    * @return name with query bolded if query can be found in the name.
    */
-  public static CharSequence getNameWithQueryBolded(@Nullable String query, @NonNull String name) {
+  public static CharSequence getNameWithQueryBolded(
+      @Nullable String query, @NonNull String name, @NonNull Context context) {
     if (TextUtils.isEmpty(query)) {
       return name;
     }
 
-    if (!QueryFilteringUtil.nameMatchesT9Query(query, name)) {
+    if (!QueryFilteringUtil.nameMatchesT9Query(query, name, context)) {
       Pattern pattern = Pattern.compile("(^|\\s)" + Pattern.quote(query.toLowerCase()));
       Matcher matcher = pattern.matcher(name.toLowerCase());
       if (matcher.find()) {
@@ -69,7 +72,7 @@
     }
 
     Pattern pattern = Pattern.compile("(^|\\s)" + Pattern.quote(query.toLowerCase()));
-    Matcher matcher = pattern.matcher(QueryFilteringUtil.getT9Representation(name));
+    Matcher matcher = pattern.matcher(QueryFilteringUtil.getT9Representation(name, context));
     if (matcher.find()) {
       // query matches the start of a T9 name (i.e. 75 -> "Jessica [Jo]nes")
       int index = matcher.start();
@@ -79,11 +82,12 @@
 
     } else {
       // query match the T9 initials (i.e. 222 -> "[A]l [B]ob [C]harlie")
-      return getNameWithInitialsBolded(query, name);
+      return getNameWithInitialsBolded(query, name, context);
     }
   }
 
-  private static CharSequence getNameWithInitialsBolded(String query, String name) {
+  private static CharSequence getNameWithInitialsBolded(
+      String query, String name, Context context) {
     SpannableString boldedInitials = new SpannableString(name);
     name = name.toLowerCase();
     int initialsBolded = 0;
@@ -91,7 +95,8 @@
 
     while (++nameIndex < name.length() && initialsBolded < query.length()) {
       if ((nameIndex == 0 || name.charAt(nameIndex - 1) == ' ')
-          && QueryFilteringUtil.getDigit(name.charAt(nameIndex)) == query.charAt(initialsBolded)) {
+          && QueryFilteringUtil.getDigit(name.charAt(nameIndex), context)
+              == query.charAt(initialsBolded)) {
         boldedInitials.setSpan(
             new StyleSpan(Typeface.BOLD),
             nameIndex,
diff --git a/java/com/android/dialer/searchfragment/common/QueryFilteringUtil.java b/java/com/android/dialer/searchfragment/common/QueryFilteringUtil.java
index 6b5cea8..1ecb486 100644
--- a/java/com/android/dialer/searchfragment/common/QueryFilteringUtil.java
+++ b/java/com/android/dialer/searchfragment/common/QueryFilteringUtil.java
@@ -16,14 +16,24 @@
 
 package com.android.dialer.searchfragment.common;
 
+import android.content.Context;
 import android.support.annotation.NonNull;
+import android.support.v4.util.SimpleArrayMap;
 import android.telephony.PhoneNumberUtils;
 import android.text.TextUtils;
+import com.android.dialer.dialpadview.DialpadCharMappings;
 import java.util.regex.Pattern;
 
 /** Utility class for filtering, comparing and handling strings and queries. */
 public class QueryFilteringUtil {
 
+  /**
+   * The default character-digit map that will be used to find the digit associated with a given
+   * character on a T9 keyboard.
+   */
+  private static final SimpleArrayMap<Character, Character> DEFAULT_CHAR_TO_DIGIT_MAP =
+      DialpadCharMappings.getDefaultCharToKeyMap();
+
   /** Matches strings with "-", "(", ")", 2-9 of at least length one. */
   private static final Pattern T9_PATTERN = Pattern.compile("[\\-()2-9]+");
 
@@ -38,15 +48,29 @@
    *   <li>#nameMatchesT9Query("56", "Jessica Jones") returns true, 56 -> 'Jo'
    *   <li>#nameMatchesT9Query("7", "Jessica Jones") returns false, no names start with P,Q,R or S
    * </ul>
+   *
+   * <p>When the 1st language preference uses a non-Latin alphabet (e.g., Russian) and the character
+   * mappings for the alphabet is defined in {@link DialpadCharMappings}, the Latin alphabet will be
+   * used first to check if the name matches the query. If they don't match, the non-Latin alphabet
+   * will be used.
+   *
+   * <p>Examples (when the 1st language preference is Russian):
+   *
+   * <ul>
+   *   <li>#nameMatchesT9Query("7", "John Smith") returns true, 7 -> 'S'
+   *   <li>#nameMatchesT9Query("7", "Павел Чехов") returns true, 7 -> 'Ч'
+   *   <li>#nameMatchesT9Query("77", "Pavel Чехов") returns true, 7 -> 'P' (in the Latin alphabet),
+   *       7 -> 'Ч' (in the Russian alphabet)
+   * </ul>
    */
-  public static boolean nameMatchesT9Query(String query, String name) {
+  public static boolean nameMatchesT9Query(String query, String name, Context context) {
     if (!T9_PATTERN.matcher(query).matches()) {
       return false;
     }
 
     query = digitsOnly(query);
     Pattern pattern = Pattern.compile("(^|\\s)" + Pattern.quote(query));
-    if (pattern.matcher(getT9Representation(name)).find()) {
+    if (pattern.matcher(getT9Representation(name, context)).find()) {
       // query matches the start of a T9 name (i.e. 75 -> "Jessica [Jo]nes")
       return true;
     }
@@ -61,7 +85,7 @@
         continue;
       }
 
-      if (getDigit(names[i].charAt(0)) == query.charAt(queryIndex)) {
+      if (getDigit(names[i].charAt(0), context) == query.charAt(queryIndex)) {
         queryIndex++;
       }
     }
@@ -106,11 +130,17 @@
     return digitsOnly(number).indexOf(digitsOnly(query));
   }
 
-  // Returns string with letters replaced with their T9 representation.
-  static String getT9Representation(String s) {
+  /**
+   * Replaces characters in the given string with their T9 representations.
+   *
+   * @param s The original string
+   * @param context The context
+   * @return The original string with characters replaced with T9 representations.
+   */
+  static String getT9Representation(String s, Context context) {
     StringBuilder builder = new StringBuilder(s.length());
     for (char c : s.toLowerCase().toCharArray()) {
-      builder.append(getDigit(c));
+      builder.append(getDigit(c, context));
     }
     return builder.toString();
   }
@@ -127,45 +157,26 @@
     return sb.toString();
   }
 
-  // Returns the T9 representation of a lower case character, otherwise returns the character.
-  static char getDigit(char c) {
-    switch (c) {
-      case 'a':
-      case 'b':
-      case 'c':
-        return '2';
-      case 'd':
-      case 'e':
-      case 'f':
-        return '3';
-      case 'g':
-      case 'h':
-      case 'i':
-        return '4';
-      case 'j':
-      case 'k':
-      case 'l':
-        return '5';
-      case 'm':
-      case 'n':
-      case 'o':
-        return '6';
-      case 'p':
-      case 'q':
-      case 'r':
-      case 's':
-        return '7';
-      case 't':
-      case 'u':
-      case 'v':
-        return '8';
-      case 'w':
-      case 'x':
-      case 'y':
-      case 'z':
-        return '9';
-      default:
-        return c;
+  /**
+   * Returns the digit on a T9 keyboard which is associated with the given lower case character.
+   *
+   * <p>The default character-key mapping will be used first to find a digit. If no digit is found,
+   * try the mapping of the current default locale if it is defined in {@link DialpadCharMappings}.
+   * If the second attempt fails, return the original character.
+   */
+  static char getDigit(char c, Context context) {
+    Character digit = DEFAULT_CHAR_TO_DIGIT_MAP.get(c);
+    if (digit != null) {
+      return digit;
     }
+
+    SimpleArrayMap<Character, Character> charToKeyMap =
+        DialpadCharMappings.getCharToKeyMap(context);
+    if (charToKeyMap != null) {
+      digit = charToKeyMap.get(c);
+      return digit != null ? digit : c;
+    }
+
+    return c;
   }
 }
diff --git a/java/com/android/dialer/searchfragment/cp2/ContactFilterCursor.java b/java/com/android/dialer/searchfragment/cp2/ContactFilterCursor.java
index 84c22a2..df67b76 100644
--- a/java/com/android/dialer/searchfragment/cp2/ContactFilterCursor.java
+++ b/java/com/android/dialer/searchfragment/cp2/ContactFilterCursor.java
@@ -17,6 +17,7 @@
 package com.android.dialer.searchfragment.cp2;
 
 import android.content.ContentResolver;
+import android.content.Context;
 import android.database.CharArrayBuffer;
 import android.database.ContentObserver;
 import android.database.Cursor;
@@ -44,7 +45,7 @@
  * Wrapper for a cursor containing all on device contacts.
  *
  * <p>This cursor removes duplicate phone numbers associated with the same contact and can filter
- * contacts based on a query by calling {@link #filter(String)}.
+ * contacts based on a query by calling {@link #filter(String, Context)}.
  */
 final class ContactFilterCursor implements Cursor {
 
@@ -72,10 +73,11 @@
   /**
    * @param cursor with projection {@link Projections#CP2_PROJECTION}.
    * @param query to filter cursor results.
+   * @param context of the app.
    */
-  ContactFilterCursor(Cursor cursor, @Nullable String query) {
+  ContactFilterCursor(Cursor cursor, @Nullable String query, Context context) {
     this.cursor = createCursor(cursor);
-    filter(query);
+    filter(query, context);
   }
 
   /**
@@ -238,7 +240,7 @@
    *   <li>Its company contains the query
    * </ul>
    */
-  public void filter(@Nullable String query) {
+  public void filter(@Nullable String query, Context context) {
     if (query == null) {
       query = "";
     }
@@ -253,7 +255,7 @@
       String companyName = cursor.getString(Projections.COMPANY_NAME);
       String nickName = cursor.getString(Projections.NICKNAME);
       if (TextUtils.isEmpty(query)
-          || QueryFilteringUtil.nameMatchesT9Query(query, name)
+          || QueryFilteringUtil.nameMatchesT9Query(query, name, context)
           || QueryFilteringUtil.numberMatchesNumberQuery(query, number)
           || QueryFilteringUtil.nameContainsQuery(query, name)
           || QueryFilteringUtil.nameContainsQuery(query, companyName)
diff --git a/java/com/android/dialer/searchfragment/cp2/SearchContactViewHolder.java b/java/com/android/dialer/searchfragment/cp2/SearchContactViewHolder.java
index c09396c..386ab3a 100644
--- a/java/com/android/dialer/searchfragment/cp2/SearchContactViewHolder.java
+++ b/java/com/android/dialer/searchfragment/cp2/SearchContactViewHolder.java
@@ -104,7 +104,7 @@
             : context.getString(
                 com.android.contacts.common.R.string.call_subject_type_and_number, label, number);
 
-    nameOrNumberView.setText(QueryBoldingUtil.getNameWithQueryBolded(query, name));
+    nameOrNumberView.setText(QueryBoldingUtil.getNameWithQueryBolded(query, name, context));
     numberView.setText(QueryBoldingUtil.getNumberWithQueryBolded(query, secondaryInfo));
     setCallToAction(cursor, query);
 
diff --git a/java/com/android/dialer/searchfragment/cp2/SearchContactsCursor.java b/java/com/android/dialer/searchfragment/cp2/SearchContactsCursor.java
index 508ca7f..7697e05 100644
--- a/java/com/android/dialer/searchfragment/cp2/SearchContactsCursor.java
+++ b/java/com/android/dialer/searchfragment/cp2/SearchContactsCursor.java
@@ -32,17 +32,19 @@
 final class SearchContactsCursor extends MergeCursor implements SearchCursor {
 
   private final ContactFilterCursor contactFilterCursor;
+  private final Context context;
 
   static SearchContactsCursor newInstance(
       Context context, ContactFilterCursor contactFilterCursor) {
     MatrixCursor headerCursor = new MatrixCursor(HEADER_PROJECTION);
     headerCursor.addRow(new String[] {context.getString(R.string.all_contacts)});
-    return new SearchContactsCursor(new Cursor[] {headerCursor, contactFilterCursor});
+    return new SearchContactsCursor(new Cursor[] {headerCursor, contactFilterCursor}, context);
   }
 
-  private SearchContactsCursor(Cursor[] cursors) {
+  private SearchContactsCursor(Cursor[] cursors, Context context) {
     super(cursors);
-    contactFilterCursor = (ContactFilterCursor) cursors[1];
+    this.contactFilterCursor = (ContactFilterCursor) cursors[1];
+    this.context = context;
   }
 
   @Override
@@ -52,7 +54,7 @@
 
   @Override
   public boolean updateQuery(@Nullable String query) {
-    contactFilterCursor.filter(query);
+    contactFilterCursor.filter(query, context);
     return true;
   }
 
diff --git a/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java b/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java
index d3abbff..3551801 100644
--- a/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java
+++ b/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java
@@ -61,7 +61,7 @@
     // All contacts
     Cursor cursor = super.loadInBackground();
     // Filtering logic
-    ContactFilterCursor contactFilterCursor = new ContactFilterCursor(cursor, query);
+    ContactFilterCursor contactFilterCursor = new ContactFilterCursor(cursor, query, getContext());
     // Header logic
     return SearchContactsCursor.newInstance(getContext(), contactFilterCursor);
   }
diff --git a/java/com/android/dialer/searchfragment/nearbyplaces/NearbyPlaceViewHolder.java b/java/com/android/dialer/searchfragment/nearbyplaces/NearbyPlaceViewHolder.java
index 5d51880..2e1fd5e 100644
--- a/java/com/android/dialer/searchfragment/nearbyplaces/NearbyPlaceViewHolder.java
+++ b/java/com/android/dialer/searchfragment/nearbyplaces/NearbyPlaceViewHolder.java
@@ -64,8 +64,8 @@
     String name = cursor.getString(Projections.DISPLAY_NAME);
     String address = cursor.getString(Projections.PHONE_LABEL);
 
-    placeName.setText(QueryBoldingUtil.getNameWithQueryBolded(query, name));
-    placeAddress.setText(QueryBoldingUtil.getNameWithQueryBolded(query, address));
+    placeName.setText(QueryBoldingUtil.getNameWithQueryBolded(query, name, context));
+    placeAddress.setText(QueryBoldingUtil.getNameWithQueryBolded(query, address, context));
     String photoUri = cursor.getString(Projections.PHOTO_URI);
     ContactPhotoManager.getInstance(context)
         .loadDialerThumbnailOrPhoto(
diff --git a/java/com/android/dialer/searchfragment/remote/RemoteContactViewHolder.java b/java/com/android/dialer/searchfragment/remote/RemoteContactViewHolder.java
index 8a02eb9..339855f 100644
--- a/java/com/android/dialer/searchfragment/remote/RemoteContactViewHolder.java
+++ b/java/com/android/dialer/searchfragment/remote/RemoteContactViewHolder.java
@@ -72,8 +72,8 @@
             : context.getString(
                 com.android.contacts.common.R.string.call_subject_type_and_number, label, number);
 
-    nameView.setText(QueryBoldingUtil.getNameWithQueryBolded(query, name));
-    numberView.setText(QueryBoldingUtil.getNameWithQueryBolded(query, secondaryInfo));
+    nameView.setText(QueryBoldingUtil.getNameWithQueryBolded(query, name, context));
+    numberView.setText(QueryBoldingUtil.getNameWithQueryBolded(query, secondaryInfo, context));
 
     if (shouldShowPhoto(cursor)) {
       nameView.setVisibility(View.VISIBLE);
diff --git a/java/com/android/incallui/incall/impl/InCallFragment.java b/java/com/android/incallui/incall/impl/InCallFragment.java
index fc31c74..73b414d 100644
--- a/java/com/android/incallui/incall/impl/InCallFragment.java
+++ b/java/com/android/incallui/incall/impl/InCallFragment.java
@@ -320,7 +320,7 @@
       }
     }
     transaction.setCustomAnimations(R.anim.abc_slide_in_top, R.anim.abc_slide_out_top);
-    transaction.commitAllowingStateLoss();
+    transaction.commitNowAllowingStateLoss();
   }
 
   @Override
diff --git a/java/com/android/voicemail/impl/transcribe/TranscriptionConfigProvider.java b/java/com/android/voicemail/impl/transcribe/TranscriptionConfigProvider.java
index f4996a0..98c8461 100644
--- a/java/com/android/voicemail/impl/transcribe/TranscriptionConfigProvider.java
+++ b/java/com/android/voicemail/impl/transcribe/TranscriptionConfigProvider.java
@@ -80,6 +80,11 @@
         .getBoolean("voicemail_transcription_donation_available", false);
   }
 
+  public boolean useClientGeneratedVoicemailIds() {
+    return ConfigProviderBindings.get(context)
+        .getBoolean("voicemail_transcription_client_generated_voicemail_ids", false);
+  }
+
   @Override
   public String toString() {
     return String.format(
diff --git a/java/com/android/voicemail/impl/transcribe/TranscriptionTaskAsync.java b/java/com/android/voicemail/impl/transcribe/TranscriptionTaskAsync.java
index f946607..808bf0f 100644
--- a/java/com/android/voicemail/impl/transcribe/TranscriptionTaskAsync.java
+++ b/java/com/android/voicemail/impl/transcribe/TranscriptionTaskAsync.java
@@ -17,6 +17,7 @@
 
 import android.app.job.JobWorkItem;
 import android.content.Context;
+import android.support.annotation.VisibleForTesting;
 import android.util.Pair;
 import com.android.dialer.common.Assert;
 import com.android.dialer.logging.DialerImpression;
@@ -121,13 +122,21 @@
     return new Pair<>(null, TranscriptionStatus.FAILED_NO_RETRY);
   }
 
+  @VisibleForTesting
   TranscribeVoicemailAsyncRequest getUploadRequest() {
-    return TranscribeVoicemailAsyncRequest.newBuilder()
-        .setVoicemailData(audioData)
-        .setAudioFormat(encoding)
-        .setDonationPreference(
-            isDonationEnabled() ? DonationPreference.DONATE : DonationPreference.DO_NOT_DONATE)
-        .build();
+    TranscribeVoicemailAsyncRequest.Builder builder =
+        TranscribeVoicemailAsyncRequest.newBuilder()
+            .setVoicemailData(audioData)
+            .setAudioFormat(encoding)
+            .setDonationPreference(
+                isDonationEnabled() ? DonationPreference.DONATE : DonationPreference.DO_NOT_DONATE);
+    // Generate the transcript id locally if configured to do so, or if voicemail donation is
+    // available (because rating donating voicemails requires locally generated voicemail ids).
+    if (configProvider.useClientGeneratedVoicemailIds()
+        || configProvider.isVoicemailDonationAvailable()) {
+      builder.setTranscriptionId(TranscriptionUtils.getFingerprintFor(audioData));
+    }
+    return builder.build();
   }
 
   private boolean isDonationEnabled() {
diff --git a/java/com/android/voicemail/impl/transcribe/TranscriptionUtils.java b/java/com/android/voicemail/impl/transcribe/TranscriptionUtils.java
new file mode 100644
index 0000000..a001f17
--- /dev/null
+++ b/java/com/android/voicemail/impl/transcribe/TranscriptionUtils.java
@@ -0,0 +1,41 @@
+/*
+ * 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.voicemail.impl.transcribe;
+
+import android.annotation.TargetApi;
+import android.os.Build.VERSION_CODES;
+import android.util.Base64;
+import com.android.dialer.common.Assert;
+import com.google.protobuf.ByteString;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/** Utility methods used by this transcription package. */
+public class TranscriptionUtils {
+
+  @TargetApi(VERSION_CODES.O)
+  static String getFingerprintFor(ByteString data) {
+    Assert.checkArgument(data != null);
+    try {
+      MessageDigest md = MessageDigest.getInstance("MD5");
+      byte[] md5Bytes = md.digest(data.toByteArray());
+      return Base64.encodeToString(md5Bytes, Base64.DEFAULT);
+    } catch (NoSuchAlgorithmException e) {
+      Assert.fail(e.toString());
+    }
+    return null;
+  }
+}