Merge "Replace $(COMMON_JAVA_PACKAGE_SUFFIX) with .jar"
diff --git a/java/com/android/dialer/app/DialtactsActivity.java b/java/com/android/dialer/app/DialtactsActivity.java
index 48796db..cc3f81b 100644
--- a/java/com/android/dialer/app/DialtactsActivity.java
+++ b/java/com/android/dialer/app/DialtactsActivity.java
@@ -109,6 +109,7 @@
 import com.android.dialer.dialpadview.DialpadFragment;
 import com.android.dialer.dialpadview.DialpadFragment.DialpadListener;
 import com.android.dialer.dialpadview.DialpadFragment.LastOutgoingCallCallback;
+import com.android.dialer.duo.DuoComponent;
 import com.android.dialer.interactions.PhoneNumberInteraction;
 import com.android.dialer.interactions.PhoneNumberInteraction.InteractionErrorCode;
 import com.android.dialer.logging.DialerImpression;
@@ -836,6 +837,10 @@
             .setActionTextColor(getResources().getColor(R.color.dialer_snackbar_action_text_color))
             .show();
       }
+    } else if (requestCode == ActivityRequestCodes.DIALTACTS_DUO) {
+      // We just returned from starting Duo for a task. Reload our reachability data since it
+      // may have changed after a user finished activating Duo.
+      DuoComponent.get(this).getDuo().reloadReachability(this);
     }
     super.onActivityResult(requestCode, resultCode, data);
   }
diff --git a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
index a5a0cff..a6489cd 100644
--- a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
+++ b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
@@ -683,7 +683,11 @@
           inviteVideoButtonView.setTag(IntentProvider.getDuoInviteIntentProvider(number));
           inviteVideoButtonView.setVisibility(View.VISIBLE);
         } else if (duo.isEnabled(mContext)) {
-          setUpVideoButtonView.setTag(IntentProvider.getSetUpDuoIntentProvider());
+          if (!duo.isInstalled(mContext)) {
+            setUpVideoButtonView.setTag(IntentProvider.getInstallDuoIntentProvider());
+          } else {
+            setUpVideoButtonView.setTag(IntentProvider.getSetUpDuoIntentProvider());
+          }
           setUpVideoButtonView.setVisibility(View.VISIBLE);
         }
         break;
diff --git a/java/com/android/dialer/app/calllog/IntentProvider.java b/java/com/android/dialer/app/calllog/IntentProvider.java
index 996bca0..c86a260 100644
--- a/java/com/android/dialer/app/calllog/IntentProvider.java
+++ b/java/com/android/dialer/app/calllog/IntentProvider.java
@@ -104,6 +104,26 @@
     };
   }
 
+  public static IntentProvider getInstallDuoIntentProvider() {
+    return new IntentProvider() {
+      @Override
+      public Intent getIntent(Context context) {
+        return new Intent(
+            Intent.ACTION_VIEW,
+            new Uri.Builder()
+                .scheme("https")
+                .authority("play.google.com")
+                .appendEncodedPath("store/apps/details")
+                .appendQueryParameter("id", "com.google.android.apps.tachyon")
+                .appendQueryParameter(
+                    "referrer",
+                    "utm_source=dialer&utm_medium=text&utm_campaign=product") // This string is from
+                // the Duo team
+                .build());
+      }
+    };
+  }
+
   public static IntentProvider getSetUpDuoIntentProvider() {
     return new IntentProvider() {
       @Override
diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailTosMessageCreator.java b/java/com/android/dialer/app/voicemail/error/VoicemailTosMessageCreator.java
index 2787320..a714b6d 100644
--- a/java/com/android/dialer/app/voicemail/error/VoicemailTosMessageCreator.java
+++ b/java/com/android/dialer/app/voicemail/error/VoicemailTosMessageCreator.java
@@ -187,7 +187,7 @@
       return true;
     }
 
-    if (isVoicemailTranscriptionEnabled() && !isLegacyVoicemailUser()) {
+    if (isVoicemailTranscriptionAvailable() && !isLegacyVoicemailUser()) {
       LogUtil.i(
           "VoicemailTosMessageCreator.shouldShowTos", "showing TOS for Google transcription users");
       return true;
@@ -203,7 +203,7 @@
       return false;
     }
 
-    if (isVoicemailTranscriptionEnabled()) {
+    if (isVoicemailTranscriptionAvailable()) {
       LogUtil.i(
           "VoicemailTosMessageCreator.shouldShowPromo",
           "showing promo for Google transcription users");
@@ -227,9 +227,10 @@
     }
   }
 
-  private boolean isVoicemailTranscriptionEnabled() {
+  private boolean isVoicemailTranscriptionAvailable() {
     return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
-        && ConfigProviderBindings.get(context).getBoolean("voicemail_transcription_enabled", false);
+        && ConfigProviderBindings.get(context)
+            .getBoolean("voicemail_transcription_available", false);
   }
 
   private void showDeclineTosDialog(final PhoneAccountHandle handle) {
@@ -407,7 +408,7 @@
   }
 
   private CharSequence getNewUserDialerTos() {
-    if (!isVoicemailTranscriptionEnabled()) {
+    if (!isVoicemailTranscriptionAvailable()) {
       return "";
     }
 
@@ -416,7 +417,7 @@
   }
 
   private CharSequence getExistingUserDialerTos() {
-    if (!isVoicemailTranscriptionEnabled()) {
+    if (!isVoicemailTranscriptionAvailable()) {
       return "";
     }
 
diff --git a/java/com/android/dialer/calldetails/CallDetailsActivity.java b/java/com/android/dialer/calldetails/CallDetailsActivity.java
index c29f9e9..b314e26 100644
--- a/java/com/android/dialer/calldetails/CallDetailsActivity.java
+++ b/java/com/android/dialer/calldetails/CallDetailsActivity.java
@@ -19,9 +19,12 @@
 import android.Manifest.permission;
 import android.annotation.SuppressLint;
 import android.app.Activity;
+import android.app.LoaderManager.LoaderCallbacks;
 import android.content.ActivityNotFoundException;
 import android.content.Context;
 import android.content.Intent;
+import android.content.Loader;
+import android.database.Cursor;
 import android.os.AsyncTask;
 import android.os.Bundle;
 import android.provider.CallLog;
@@ -35,11 +38,13 @@
 import android.support.v7.widget.Toolbar;
 import android.view.View;
 import android.widget.Toast;
+import com.android.dialer.CoalescedIds;
 import com.android.dialer.DialerPhoneNumber;
 import com.android.dialer.assisteddialing.ui.AssistedDialingSettingActivity;
 import com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry;
 import com.android.dialer.callintent.CallInitiationType;
 import com.android.dialer.callintent.CallIntentBuilder;
+import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.common.concurrent.AsyncTaskExecutors;
@@ -62,6 +67,7 @@
 import com.android.dialer.postcall.PostCall;
 import com.android.dialer.precall.PreCall;
 import com.android.dialer.protos.ProtoParsers;
+import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
 import com.google.i18n.phonenumbers.PhoneNumberUtil;
 import java.lang.ref.WeakReference;
@@ -71,10 +77,12 @@
 
 /** Displays the details of a specific call log entry. */
 public class CallDetailsActivity extends AppCompatActivity {
+  private static final int CALL_DETAILS_LOADER_ID = 0;
 
   public static final String EXTRA_PHONE_NUMBER = "phone_number";
   public static final String EXTRA_HAS_ENRICHED_CALL_DATA = "has_enriched_call_data";
   public static final String EXTRA_CALL_DETAILS_ENTRIES = "call_details_entries";
+  public static final String EXTRA_COALESCED_CALL_LOG_IDS = "coalesced_call_log_ids";
   public static final String EXTRA_CONTACT = "contact";
   public static final String EXTRA_CAN_REPORT_CALLER_ID = "can_report_caller_id";
   private static final String EXTRA_CAN_SUPPORT_ASSISTED_DIALING = "can_support_assisted_dialing";
@@ -93,23 +101,47 @@
   private DialerContact contact;
   private CallDetailsAdapter adapter;
 
+  // This will be present only when the activity is launched from the new call log UI, i.e., a list
+  // of coalesced annotated call log IDs is included in the intent.
+  private Optional<CoalescedIds> coalescedCallLogIds = Optional.absent();
+
   public static boolean isLaunchIntent(Intent intent) {
     return intent.getComponent() != null
         && CallDetailsActivity.class.getName().equals(intent.getComponent().getClassName());
   }
 
+  /**
+   * Returns an {@link Intent} for launching the {@link CallDetailsActivity} from the old call log
+   * UI.
+   */
   public static Intent newInstance(
       Context context,
-      @NonNull CallDetailsEntries details,
-      @NonNull DialerContact contact,
+      CallDetailsEntries details,
+      DialerContact contact,
       boolean canReportCallerId,
       boolean canSupportAssistedDialing) {
-    Assert.isNotNull(details);
-    Assert.isNotNull(contact);
-
     Intent intent = new Intent(context, CallDetailsActivity.class);
-    ProtoParsers.put(intent, EXTRA_CONTACT, contact);
-    ProtoParsers.put(intent, EXTRA_CALL_DETAILS_ENTRIES, details);
+    ProtoParsers.put(intent, EXTRA_CONTACT, Assert.isNotNull(contact));
+    ProtoParsers.put(intent, EXTRA_CALL_DETAILS_ENTRIES, Assert.isNotNull(details));
+    intent.putExtra(EXTRA_CAN_REPORT_CALLER_ID, canReportCallerId);
+    intent.putExtra(EXTRA_CAN_SUPPORT_ASSISTED_DIALING, canSupportAssistedDialing);
+    return intent;
+  }
+
+  /**
+   * Returns an {@link Intent} for launching the {@link CallDetailsActivity} from the new call log
+   * UI.
+   */
+  public static Intent newInstance(
+      Context context,
+      CoalescedIds coalescedAnnotatedCallLogIds,
+      DialerContact contact,
+      boolean canReportCallerId,
+      boolean canSupportAssistedDialing) {
+    Intent intent = new Intent(context, CallDetailsActivity.class);
+    ProtoParsers.put(intent, EXTRA_CONTACT, Assert.isNotNull(contact));
+    ProtoParsers.put(
+        intent, EXTRA_COALESCED_CALL_LOG_IDS, Assert.isNotNull(coalescedAnnotatedCallLogIds));
     intent.putExtra(EXTRA_CAN_REPORT_CALLER_ID, canReportCallerId);
     intent.putExtra(EXTRA_CAN_SUPPORT_ASSISTED_DIALING, canSupportAssistedDialing);
     return intent;
@@ -166,10 +198,30 @@
   }
 
   private void onHandleIntent(Intent intent) {
+    boolean hasCallDetailsEntries = intent.hasExtra(EXTRA_CALL_DETAILS_ENTRIES);
+    boolean hasCoalescedCallLogIds = intent.hasExtra(EXTRA_COALESCED_CALL_LOG_IDS);
+    Assert.checkArgument(
+        (hasCallDetailsEntries && !hasCoalescedCallLogIds)
+            || (!hasCallDetailsEntries && hasCoalescedCallLogIds),
+        "One and only one of EXTRA_CALL_DETAILS_ENTRIES and EXTRA_COALESCED_CALL_LOG_IDS "
+            + "can be included in the intent.");
+
     contact = ProtoParsers.getTrusted(intent, EXTRA_CONTACT, DialerContact.getDefaultInstance());
-    entries =
-        ProtoParsers.getTrusted(
-            intent, EXTRA_CALL_DETAILS_ENTRIES, CallDetailsEntries.getDefaultInstance());
+    if (hasCallDetailsEntries) {
+      entries =
+          ProtoParsers.getTrusted(
+              intent, EXTRA_CALL_DETAILS_ENTRIES, CallDetailsEntries.getDefaultInstance());
+    } else {
+      entries = CallDetailsEntries.getDefaultInstance();
+      coalescedCallLogIds =
+          Optional.of(
+              ProtoParsers.getTrusted(
+                  intent, EXTRA_COALESCED_CALL_LOG_IDS, CoalescedIds.getDefaultInstance()));
+      getLoaderManager()
+          .initLoader(
+              CALL_DETAILS_LOADER_ID, /* args = */ null, new CallDetailsLoaderCallbacks(this));
+    }
+
     adapter =
         new CallDetailsAdapter(
             this /* context */,
@@ -191,6 +243,43 @@
     super.onBackPressed();
   }
 
+  /**
+   * {@link LoaderCallbacks} for {@link CallDetailsCursorLoader}, which loads call detail entries
+   * from {@link AnnotatedCallLog}.
+   */
+  private static final class CallDetailsLoaderCallbacks implements LoaderCallbacks<Cursor> {
+    private final CallDetailsActivity activity;
+
+    CallDetailsLoaderCallbacks(CallDetailsActivity callDetailsActivity) {
+      this.activity = callDetailsActivity;
+    }
+
+    @Override
+    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+      Assert.checkState(activity.coalescedCallLogIds.isPresent());
+
+      return new CallDetailsCursorLoader(activity, activity.coalescedCallLogIds.get());
+    }
+
+    @Override
+    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+      updateCallDetailsEntries(CallDetailsCursorLoader.toCallDetailsEntries(data));
+    }
+
+    @Override
+    public void onLoaderReset(Loader<Cursor> loader) {
+      updateCallDetailsEntries(CallDetailsEntries.getDefaultInstance());
+    }
+
+    private void updateCallDetailsEntries(CallDetailsEntries newEntries) {
+      activity.entries = newEntries;
+      activity.adapter.updateCallDetailsEntries(newEntries.getEntriesList());
+      EnrichedCallComponent.get(activity)
+          .getEnrichedCallManager()
+          .requestAllHistoricalData(activity.contact.getNumber(), newEntries);
+    }
+  }
+
   /** Delete specified calls from the call log. */
   private static class DeleteCallsTask extends AsyncTask<Void, Void, Void> {
     // Use a weak reference to hold the Activity so that there is no memory leak.
diff --git a/java/com/android/dialer/calldetails/CallDetailsAdapter.java b/java/com/android/dialer/calldetails/CallDetailsAdapter.java
index 9095b86..030366e 100644
--- a/java/com/android/dialer/calldetails/CallDetailsAdapter.java
+++ b/java/com/android/dialer/calldetails/CallDetailsAdapter.java
@@ -115,7 +115,9 @@
 
   @Override
   public int getItemCount() {
-    return callDetailsEntries.size() + 2; // Header + footer
+    return callDetailsEntries.isEmpty()
+        ? 0
+        : callDetailsEntries.size() + 2; // plus header and footer
   }
 
   void updateCallDetailsEntries(List<CallDetailsEntry> entries) {
diff --git a/java/com/android/dialer/calldetails/CallDetailsCursorLoader.java b/java/com/android/dialer/calldetails/CallDetailsCursorLoader.java
new file mode 100644
index 0000000..8385253
--- /dev/null
+++ b/java/com/android/dialer/calldetails/CallDetailsCursorLoader.java
@@ -0,0 +1,139 @@
+/*
+ * 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.calldetails;
+
+import android.content.Context;
+import android.content.CursorLoader;
+import android.database.Cursor;
+import com.android.dialer.CoalescedIds;
+import com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry;
+import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog;
+import com.android.dialer.common.Assert;
+import com.android.dialer.duo.DuoConstants;
+
+/**
+ * A {@link CursorLoader} that loads call detail entries from {@link AnnotatedCallLog} for {@link
+ * CallDetailsActivity}.
+ */
+public final class CallDetailsCursorLoader extends CursorLoader {
+
+  // Columns in AnnotatedCallLog that are needed to build a CallDetailsEntry proto.
+  // Be sure to update (1) constants that store indexes of the elements and (2) method
+  // toCallDetailsEntry(Cursor) when updating this array.
+  public static final String[] COLUMNS_FOR_CALL_DETAILS =
+      new String[] {
+        AnnotatedCallLog._ID,
+        AnnotatedCallLog.CALL_TYPE,
+        AnnotatedCallLog.FEATURES,
+        AnnotatedCallLog.TIMESTAMP,
+        AnnotatedCallLog.DURATION,
+        AnnotatedCallLog.DATA_USAGE,
+        AnnotatedCallLog.PHONE_ACCOUNT_COMPONENT_NAME
+      };
+
+  // Indexes for COLUMNS_FOR_CALL_DETAILS
+  private static final int ID = 0;
+  private static final int CALL_TYPE = 1;
+  private static final int FEATURES = 2;
+  private static final int TIMESTAMP = 3;
+  private static final int DURATION = 4;
+  private static final int DATA_USAGE = 5;
+  private static final int PHONE_ACCOUNT_COMPONENT_NAME = 6;
+
+  CallDetailsCursorLoader(Context context, CoalescedIds coalescedIds) {
+    super(
+        context,
+        AnnotatedCallLog.CONTENT_URI,
+        COLUMNS_FOR_CALL_DETAILS,
+        annotatedCallLogIdsSelection(coalescedIds),
+        annotatedCallLogIdsSelectionArgs(coalescedIds),
+        AnnotatedCallLog.TIMESTAMP + " DESC");
+  }
+
+  /**
+   * Build a string of the form "COLUMN_NAME IN (?, ?, ..., ?)", where COLUMN_NAME is the name of
+   * the ID column in {@link AnnotatedCallLog}.
+   *
+   * <p>This string will be used as the {@code selection} parameter to initialize the loader.
+   */
+  private static String annotatedCallLogIdsSelection(CoalescedIds coalescedIds) {
+    // First, build a string of question marks ('?') separated by commas (',').
+    StringBuilder questionMarks = new StringBuilder();
+    for (int i = 0; i < coalescedIds.getCoalescedIdCount(); i++) {
+      if (i != 0) {
+        questionMarks.append(", ");
+      }
+      questionMarks.append("?");
+    }
+
+    return AnnotatedCallLog._ID + " IN (" + questionMarks + ")";
+  }
+
+  /**
+   * Returns a string that will be used as the {@code selectionArgs} parameter to initialize the
+   * loader.
+   */
+  private static String[] annotatedCallLogIdsSelectionArgs(CoalescedIds coalescedIds) {
+    String[] args = new String[coalescedIds.getCoalescedIdCount()];
+
+    for (int i = 0; i < coalescedIds.getCoalescedIdCount(); i++) {
+      args[i] = String.valueOf(coalescedIds.getCoalescedId(i));
+    }
+
+    return args;
+  }
+
+  /**
+   * Creates a new {@link CallDetailsEntries} from the entire data set loaded by this loader.
+   *
+   * @param cursor A cursor pointing to the data set loaded by this loader. The caller must ensure
+   *     the cursor is not null and the data set it points to is not empty.
+   * @return A {@link CallDetailsEntries} proto.
+   */
+  static CallDetailsEntries toCallDetailsEntries(Cursor cursor) {
+    Assert.isNotNull(cursor);
+    Assert.checkArgument(cursor.moveToFirst());
+
+    CallDetailsEntries.Builder entries = CallDetailsEntries.newBuilder();
+
+    do {
+      entries.addEntries(toCallDetailsEntry(cursor));
+    } while (cursor.moveToNext());
+
+    return entries.build();
+  }
+
+  /** Creates a new {@link CallDetailsEntry} from the provided cursor using its current position. */
+  private static CallDetailsEntry toCallDetailsEntry(Cursor cursor) {
+    CallDetailsEntry.Builder entry = CallDetailsEntry.newBuilder();
+    entry
+        .setCallId(cursor.getLong(ID))
+        .setCallType(cursor.getInt(CALL_TYPE))
+        .setFeatures(cursor.getInt(FEATURES))
+        .setDate(cursor.getLong(TIMESTAMP))
+        .setDuration(cursor.getLong(DURATION))
+        .setDataUsage(cursor.getLong(DATA_USAGE));
+
+    String phoneAccountComponentName = cursor.getString(PHONE_ACCOUNT_COMPONENT_NAME);
+    entry.setIsDuoCall(
+        DuoConstants.PHONE_ACCOUNT_COMPONENT_NAME
+            .flattenToString()
+            .equals(phoneAccountComponentName));
+
+    return entry.build();
+  }
+}
diff --git a/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java b/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java
index 93e8414..fbb5831 100644
--- a/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java
+++ b/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java
@@ -197,7 +197,7 @@
               }
               // Also save the updated information so that it can be written to PhoneLookupHistory
               // in onSuccessfulFill.
-              String normalizedNumber = dialerPhoneNumberUtil.formatToE164(dialerPhoneNumber);
+              String normalizedNumber = dialerPhoneNumberUtil.normalizeNumber(dialerPhoneNumber);
               phoneLookupHistoryRowsToUpdate.put(normalizedNumber, upToDateInfo);
             }
           }
@@ -369,7 +369,7 @@
     DialerPhoneNumberUtil dialerPhoneNumberUtil =
         new DialerPhoneNumberUtil(PhoneNumberUtil.getInstance());
     Map<DialerPhoneNumber, String> dialerPhoneNumberToNormalizedNumbers =
-        Maps.asMap(uniqueDialerPhoneNumbers, dialerPhoneNumberUtil::formatToE164);
+        Maps.asMap(uniqueDialerPhoneNumbers, dialerPhoneNumberUtil::normalizeNumber);
 
     // Convert values to a set to remove any duplicates that are the result of two
     // DialerPhoneNumbers mapping to the same normalized number.
@@ -508,7 +508,7 @@
     for (Entry<DialerPhoneNumber, Set<Long>> entry : annotatedCallLogIdsByNumber.entrySet()) {
       DialerPhoneNumber dialerPhoneNumber = entry.getKey();
       Set<Long> idsForDialerPhoneNumber = entry.getValue();
-      String normalizedNumber = dialerPhoneNumberUtil.formatToE164(dialerPhoneNumber);
+      String normalizedNumber = dialerPhoneNumberUtil.normalizeNumber(dialerPhoneNumber);
       Set<Long> idsForNormalizedNumber = idsByNormalizedNumber.get(normalizedNumber);
       if (idsForNormalizedNumber == null) {
         idsForNormalizedNumber = new ArraySet<>();
diff --git a/java/com/android/dialer/calllog/ui/menu/Modules.java b/java/com/android/dialer/calllog/ui/menu/Modules.java
index 550e284..cccaa73 100644
--- a/java/com/android/dialer/calllog/ui/menu/Modules.java
+++ b/java/com/android/dialer/calllog/ui/menu/Modules.java
@@ -24,7 +24,6 @@
 import android.telecom.PhoneAccountHandle;
 import android.text.TextUtils;
 import com.android.dialer.calldetails.CallDetailsActivity;
-import com.android.dialer.calldetails.CallDetailsEntries;
 import com.android.dialer.callintent.CallInitiationType;
 import com.android.dialer.calllog.model.CoalescedRow;
 import com.android.dialer.calllogutils.PhoneAccountUtils;
@@ -65,7 +64,7 @@
 
     // TODO(zachh): Revisit if DialerContact is the best thing to pass to CallDetails; could
     // it use a ContactPrimaryActionInfo instead?
-    addModuleForAccessingCallDetails(context, createDialerContactFromRow(row), modules);
+    addModuleForAccessingCallDetails(context, row, modules);
 
     return modules;
   }
@@ -182,10 +181,9 @@
   }
 
   private static void addModuleForAccessingCallDetails(
-      Context context, DialerContact dialerContact, List<ContactActionModule> modules) {
-    // TODO(zachh): Load CallDetailsEntries and canReportInaccurateNumber in
-    // CallDetailsActivity (see also isPeopleApiSource(sourceType)).
-    CallDetailsEntries callDetailsEntries = CallDetailsEntries.getDefaultInstance();
+      Context context, CoalescedRow row, List<ContactActionModule> modules) {
+    // TODO(zachh): Load canReportInaccurateNumber in CallDetailsActivity
+    // (see also isPeopleApiSource(sourceType)).
     boolean canReportInaccurateNumber = false;
     boolean canSupportAssistedDialing = false; // TODO(zachh): Properly set value.
 
@@ -194,8 +192,8 @@
             context,
             CallDetailsActivity.newInstance(
                 context,
-                callDetailsEntries,
-                dialerContact,
+                row.coalescedIds(),
+                createDialerContactFromRow(row),
                 canReportInaccurateNumber,
                 canSupportAssistedDialing),
             R.string.call_details_menu_label,
diff --git a/java/com/android/dialer/database/DialerDatabaseHelper.java b/java/com/android/dialer/database/DialerDatabaseHelper.java
index 9a25812..113e863 100644
--- a/java/com/android/dialer/database/DialerDatabaseHelper.java
+++ b/java/com/android/dialer/database/DialerDatabaseHelper.java
@@ -26,19 +26,21 @@
 import android.database.sqlite.SQLiteOpenHelper;
 import android.database.sqlite.SQLiteStatement;
 import android.net.Uri;
-import android.os.AsyncTask;
 import android.provider.BaseColumns;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.Directory;
+import android.support.annotation.Nullable;
 import android.support.annotation.VisibleForTesting;
 import android.support.annotation.WorkerThread;
 import android.text.TextUtils;
 import com.android.contacts.common.R;
 import com.android.contacts.common.util.StopWatch;
 import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.concurrent.DialerExecutor.Worker;
+import com.android.dialer.common.concurrent.DialerExecutorComponent;
 import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
 import com.android.dialer.smartdial.SmartDialNameMatcher;
 import com.android.dialer.smartdial.SmartDialPrefix;
@@ -332,7 +334,11 @@
   /** Starts the database upgrade process in the background. */
   public void startSmartDialUpdateThread() {
     if (PermissionsUtil.hasContactsReadPermissions(mContext)) {
-      new SmartDialUpdateAsyncTask().execute();
+      DialerExecutorComponent.get(mContext)
+          .dialerExecutorFactory()
+          .createNonUiTaskBuilder(new UpdateSmartDialWorker())
+          .build()
+          .executeParallel(null);
     }
   }
 
@@ -1228,10 +1234,11 @@
     }
   }
 
-  private class SmartDialUpdateAsyncTask extends AsyncTask<Object, Object, Object> {
+  private class UpdateSmartDialWorker implements Worker<Void, Void> {
 
+    @Nullable
     @Override
-    protected Object doInBackground(Object... objects) {
+    public Void doInBackground(@Nullable Void input) throws Throwable {
       updateSmartDialDatabase();
       return null;
     }
diff --git a/java/com/android/dialer/duo/Duo.java b/java/com/android/dialer/duo/Duo.java
index 9012dee..d914784 100644
--- a/java/com/android/dialer/duo/Duo.java
+++ b/java/com/android/dialer/duo/Duo.java
@@ -30,40 +30,65 @@
 /** Interface for Duo video call integration. */
 public interface Duo {
 
+  /** @return true if the Duo integration is enabled on this device. */
   boolean isEnabled(@NonNull Context context);
 
+  /** @return true if Duo is installed on this device. */
+  boolean isInstalled(@NonNull Context context);
+
   /**
    * @return true if Duo is installed and the user has gone through the set-up flow confirming their
    *     phone number.
    */
   boolean isActivated(@NonNull Context context);
 
+  /** @return true if the parameter number is reachable on Duo. */
   @MainThread
   boolean isReachable(@NonNull Context context, @Nullable String number);
 
-  /** @return {@code null} if result is unknown. */
+  /**
+   * @return true if the number supports upgrading a voice call to a Duo video call. Returns {@code
+   *     null} if result is unknown.
+   */
   @MainThread
   Optional<Boolean> supportsUpgrade(@NonNull Context context, @Nullable String number);
 
+  /** Starts a task to update the reachability of the parameter numbers asynchronously. */
   @MainThread
   void updateReachability(@NonNull Context context, @NonNull List<String> numbers);
 
+  /**
+   * Clears the current reachability data and starts a task to load the latest reachability data
+   * asynchronously.
+   */
+  @MainThread
+  void reloadReachability(@NonNull Context context);
+
+  /**
+   * @return an Intent to start a Duo video call with the parameter number. Must be started using
+   *     startActivityForResult.
+   */
   @MainThread
   Intent getIntent(@NonNull Context context, @NonNull String number);
 
+  /** Requests upgrading the parameter ongoing call to a Duo video call. */
   @MainThread
   void requestUpgrade(@NonNull Context context, Call call);
 
+  /** Registers a listener for reachability data changes. */
   @MainThread
   void registerListener(@NonNull DuoListener listener);
 
+  /** Unregisters a listener for reachability data changes. */
   @MainThread
   void unregisterListener(@NonNull DuoListener listener);
 
+  /** The string resource to use for outgoing Duo call entries in call details. */
   @StringRes
   @MainThread
   int getOutgoingCallTypeText();
 
+  /** The string resource to use for incoming Duo call entries in call details. */
   @StringRes
   @MainThread
   int getIncomingCallTypeText();
diff --git a/java/com/android/dialer/duo/stub/DuoStub.java b/java/com/android/dialer/duo/stub/DuoStub.java
index 628d6dc..cfa400a 100644
--- a/java/com/android/dialer/duo/stub/DuoStub.java
+++ b/java/com/android/dialer/duo/stub/DuoStub.java
@@ -41,6 +41,11 @@
   }
 
   @Override
+  public boolean isInstalled(@NonNull Context context) {
+    return false;
+  }
+
+  @Override
   public boolean isActivated(@NonNull Context context) {
     return false;
   }
@@ -68,6 +73,9 @@
     Assert.isNotNull(numbers);
   }
 
+  @Override
+  public void reloadReachability(@NonNull Context context) {}
+
   @MainThread
   @Override
   public Intent getIntent(@NonNull Context context, @NonNull String number) {
diff --git a/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java b/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java
index 3829a8d..501b088 100644
--- a/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java
+++ b/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java
@@ -30,17 +30,21 @@
 import android.text.TextUtils;
 import com.android.dialer.DialerPhoneNumber;
 import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
 import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor;
 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.android.dialer.phonelookup.PhoneLookupInfo.Cp2Info.Cp2ContactInfo;
+import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil;
 import com.android.dialer.storage.Unencrypted;
+import com.google.common.base.Optional;
 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.ListeningExecutorService;
+import com.google.i18n.phonenumbers.PhoneNumberUtil;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Objects;
@@ -61,8 +65,9 @@
         Phone.TYPE, // 3
         Phone.LABEL, // 4
         Phone.NORMALIZED_NUMBER, // 5
-        Phone.CONTACT_ID, // 6
-        Phone.LOOKUP_KEY // 7
+        Phone.NUMBER, // 6
+        Phone.CONTACT_ID, // 7
+        Phone.LOOKUP_KEY // 8
       };
 
   private static final int CP2_INFO_NAME_INDEX = 0;
@@ -70,9 +75,10 @@
   private static final int CP2_INFO_PHOTO_ID_INDEX = 2;
   private static final int CP2_INFO_TYPE_INDEX = 3;
   private static final int CP2_INFO_LABEL_INDEX = 4;
-  private static final int CP2_INFO_NUMBER_INDEX = 5;
-  private static final int CP2_INFO_CONTACT_ID_INDEX = 6;
-  private static final int CP2_INFO_LOOKUP_KEY_INDEX = 7;
+  private static final int CP2_INFO_NORMALIZED_NUMBER_INDEX = 5;
+  private static final int CP2_INFO_NUMBER_INDEX = 6;
+  private static final int CP2_INFO_CONTACT_ID_INDEX = 7;
+  private static final int CP2_INFO_LOOKUP_KEY_INDEX = 8;
 
   private final Context appContext;
   private final SharedPreferences sharedPreferences;
@@ -108,19 +114,37 @@
         || contactsDeleted(lastModified);
   }
 
-  /** Returns set of contact ids that correspond to {@code phoneNumbers} if the contact exists. */
-  private Set<Long> queryPhoneTableForContactIds(ImmutableSet<DialerPhoneNumber> phoneNumbers) {
+  /**
+   * Returns set of contact ids that correspond to {@code dialerPhoneNumbers} if the contact exists.
+   */
+  private Set<Long> queryPhoneTableForContactIds(
+      ImmutableSet<DialerPhoneNumber> dialerPhoneNumbers) {
     Set<Long> contactIds = new ArraySet<>();
+
+    PartitionedNumbers partitionedNumbers = new PartitionedNumbers(dialerPhoneNumbers);
+
+    // First use the E164 numbers to query the NORMALIZED_NUMBER column.
+    contactIds.addAll(
+        queryPhoneTableForContactIdsBasedOnE164(partitionedNumbers.validE164Numbers()));
+
+    // Then run a separate query using the NUMBER column to handle numbers that can't be formatted.
+    contactIds.addAll(
+        queryPhoneTableForContactIdsBasedOnRawNumber(partitionedNumbers.unformattableNumbers()));
+
+    return contactIds;
+  }
+
+  private Set<Long> queryPhoneTableForContactIdsBasedOnE164(Set<String> validE164Numbers) {
+    Set<Long> contactIds = new ArraySet<>();
+    if (validE164Numbers.isEmpty()) {
+      return contactIds;
+    }
     try (Cursor cursor =
-        appContext
-            .getContentResolver()
-            .query(
-                Phone.CONTENT_URI,
-                new String[] {Phone.CONTACT_ID},
-                Phone.NORMALIZED_NUMBER + " IN (" + questionMarks(phoneNumbers.size()) + ")",
-                contactIdsSelectionArgs(phoneNumbers),
-                null)) {
-      cursor.moveToPosition(-1);
+        queryPhoneTableBasedOnE164(new String[] {Phone.CONTACT_ID}, validE164Numbers)) {
+      if (cursor == null) {
+        LogUtil.w("Cp2PhoneLookup.queryPhoneTableForContactIdsBasedOnE164", "null cursor");
+        return contactIds;
+      }
       while (cursor.moveToNext()) {
         contactIds.add(cursor.getLong(0 /* columnIndex */));
       }
@@ -128,18 +152,22 @@
     return contactIds;
   }
 
-  private static String[] contactIdsSelectionArgs(ImmutableSet<DialerPhoneNumber> phoneNumbers) {
-    String[] args = new String[phoneNumbers.size()];
-    int i = 0;
-    for (DialerPhoneNumber phoneNumber : phoneNumbers) {
-      args[i++] = getNormalizedNumber(phoneNumber);
+  private Set<Long> queryPhoneTableForContactIdsBasedOnRawNumber(Set<String> unformattableNumbers) {
+    Set<Long> contactIds = new ArraySet<>();
+    if (unformattableNumbers.isEmpty()) {
+      return contactIds;
     }
-    return args;
-  }
-
-  private static String getNormalizedNumber(DialerPhoneNumber phoneNumber) {
-    // TODO(calderwoodra): implement normalization logic that matches contacts.
-    return phoneNumber.getRawInput().getNumber();
+    try (Cursor cursor =
+        queryPhoneTableBasedOnRawNumber(new String[] {Phone.CONTACT_ID}, unformattableNumbers)) {
+      if (cursor == null) {
+        LogUtil.w("Cp2PhoneLookup.queryPhoneTableForContactIdsBasedOnE164", "null cursor");
+        return contactIds;
+      }
+      while (cursor.moveToNext()) {
+        contactIds.add(cursor.getLong(0 /* columnIndex */));
+      }
+    }
+    return contactIds;
   }
 
   /** Returns true if any contacts were modified after {@code lastModified}. */
@@ -188,6 +216,10 @@
                 DeletedContacts.CONTACT_DELETED_TIMESTAMP + " > ?",
                 new String[] {Long.toString(lastModified)},
                 null)) {
+      if (cursor == null) {
+        LogUtil.w("Cp2PhoneLookup.contactsDeleted", "null cursor");
+        return false;
+      }
       return cursor.getCount() > 0;
     }
   }
@@ -312,40 +344,65 @@
 
     // 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);
-      int lastUpdatedIndex = cursor.getColumnIndex(Contacts.CONTACT_LAST_UPDATED_TIMESTAMP);
-      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 Cp2ContactInfo need to be updated.
-        long contactId = cursor.getLong(contactIdIndex);
-        updatedNumbers.addAll(getDialerPhoneNumber(existingInfoMap, contactId));
-        long lastUpdatedTimestamp = cursor.getLong(lastUpdatedIndex);
-        if (currentLastTimestampProcessed == null
-            || currentLastTimestampProcessed < lastUpdatedTimestamp) {
-          currentLastTimestampProcessed = lastUpdatedTimestamp;
+    if (!contactIds.isEmpty()) {
+      try (Cursor cursor = queryContactsTableForContacts(contactIds, lastModified)) {
+        int contactIdIndex = cursor.getColumnIndex(Contacts._ID);
+        int lastUpdatedIndex = cursor.getColumnIndex(Contacts.CONTACT_LAST_UPDATED_TIMESTAMP);
+        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 Cp2ContactInfo need to be updated.
+          long contactId = cursor.getLong(contactIdIndex);
+          updatedNumbers.addAll(
+              findDialerPhoneNumbersContainingContactId(existingInfoMap, contactId));
+          long lastUpdatedTimestamp = cursor.getLong(lastUpdatedIndex);
+          if (currentLastTimestampProcessed == null
+              || currentLastTimestampProcessed < lastUpdatedTimestamp) {
+            currentLastTimestampProcessed = lastUpdatedTimestamp;
+          }
         }
       }
     }
 
-    // Query the Phone table and build Cp2ContactInfo for each DialerPhoneNumber in our
-    // updatedNumbers set.
+    if (updatedNumbers.isEmpty()) {
+      return new ArrayMap<>();
+    }
+
     Map<DialerPhoneNumber, Set<Cp2ContactInfo>> 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));
-        Cp2ContactInfo info = buildCp2ContactInfoFromUpdatedContactsCursor(appContext, cursor);
-        for (DialerPhoneNumber phoneNumber : phoneNumbers) {
-          if (map.containsKey(phoneNumber)) {
-            map.get(phoneNumber).add(info);
-          } else {
-            Set<Cp2ContactInfo> cp2ContactInfos = new ArraySet<>();
-            cp2ContactInfos.add(info);
-            map.put(phoneNumber, cp2ContactInfos);
+
+    // Divide the numbers into those we can format to E164 and those we can't. Then run separate
+    // queries against the contacts table using the NORMALIZED_NUMBER and NUMBER columns.
+    // TODO(zachh): These queries are inefficient without a lastModified column to filter on.
+    PartitionedNumbers partitionedNumbers = new PartitionedNumbers(updatedNumbers);
+    if (!partitionedNumbers.validE164Numbers().isEmpty()) {
+      try (Cursor cursor =
+          queryPhoneTableBasedOnE164(CP2_INFO_PROJECTION, partitionedNumbers.validE164Numbers())) {
+        if (cursor == null) {
+          LogUtil.w("Cp2PhoneLookup.buildMapForUpdatedOrAddedContacts", "null cursor");
+        } else {
+          while (cursor.moveToNext()) {
+            String e164Number = cursor.getString(CP2_INFO_NORMALIZED_NUMBER_INDEX);
+            Set<DialerPhoneNumber> dialerPhoneNumbers =
+                partitionedNumbers.dialerPhoneNumbersForE164(e164Number);
+            Cp2ContactInfo info = buildCp2ContactInfoFromUpdatedContactsCursor(appContext, cursor);
+            addInfo(map, dialerPhoneNumbers, info);
+          }
+        }
+      }
+    }
+    if (!partitionedNumbers.unformattableNumbers().isEmpty()) {
+      try (Cursor cursor =
+          queryPhoneTableBasedOnRawNumber(
+              CP2_INFO_PROJECTION, partitionedNumbers.unformattableNumbers())) {
+        if (cursor == null) {
+          LogUtil.w("Cp2PhoneLookup.buildMapForUpdatedOrAddedContacts", "null cursor");
+        } else {
+          while (cursor.moveToNext()) {
+            String unformattableNumber = cursor.getString(CP2_INFO_NUMBER_INDEX);
+            Set<DialerPhoneNumber> dialerPhoneNumbers =
+                partitionedNumbers.dialerPhoneNumbersForUnformattable(unformattableNumber);
+            Cp2ContactInfo info = buildCp2ContactInfoFromUpdatedContactsCursor(appContext, cursor);
+            addInfo(map, dialerPhoneNumbers, info);
           }
         }
       }
@@ -354,20 +411,45 @@
   }
 
   /**
-   * Returns cursor with projection {@link #CP2_INFO_PROJECTION} and only phone numbers that are in
-   * {@code updateNumbers}.
+   * Adds the {@code cp2ContactInfo} to the entries for all specified {@code dialerPhoneNumbers} in
+   * the {@code map}.
    */
-  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);
+  private static void addInfo(
+      Map<DialerPhoneNumber, Set<Cp2ContactInfo>> map,
+      Set<DialerPhoneNumber> dialerPhoneNumbers,
+      Cp2ContactInfo cp2ContactInfo) {
+    for (DialerPhoneNumber dialerPhoneNumber : dialerPhoneNumbers) {
+      if (map.containsKey(dialerPhoneNumber)) {
+        map.get(dialerPhoneNumber).add(cp2ContactInfo);
+      } else {
+        Set<Cp2ContactInfo> cp2ContactInfos = new ArraySet<>();
+        cp2ContactInfos.add(cp2ContactInfo);
+        map.put(dialerPhoneNumber, cp2ContactInfos);
+      }
     }
+  }
 
+  private Cursor queryPhoneTableBasedOnE164(String[] projection, Set<String> validE164Numbers) {
     return appContext
         .getContentResolver()
-        .query(Phone.CONTENT_URI, CP2_INFO_PROJECTION, where, selectionArgs, null);
+        .query(
+            Phone.CONTENT_URI,
+            projection,
+            Phone.NORMALIZED_NUMBER + " IN (" + questionMarks(validE164Numbers.size()) + ")",
+            validE164Numbers.toArray(new String[validE164Numbers.size()]),
+            null);
+  }
+
+  private Cursor queryPhoneTableBasedOnRawNumber(
+      String[] projection, Set<String> unformattableNumbers) {
+    return appContext
+        .getContentResolver()
+        .query(
+            Phone.CONTENT_URI,
+            projection,
+            Phone.NUMBER + " IN (" + questionMarks(unformattableNumbers.size()) + ")",
+            unformattableNumbers.toArray(new String[unformattableNumbers.size()]),
+            null);
   }
 
   /**
@@ -466,7 +548,8 @@
     cursor.moveToPosition(-1);
     while (cursor.moveToNext()) {
       long contactId = cursor.getLong(contactIdIndex);
-      deletedPhoneNumbers.addAll(getDialerPhoneNumber(existingInfoMap, contactId));
+      deletedPhoneNumbers.addAll(
+          findDialerPhoneNumbersContainingContactId(existingInfoMap, contactId));
       long deletedTime = cursor.getLong(deletedTimeIndex);
       if (currentLastTimestampProcessed == null || currentLastTimestampProcessed < deletedTime) {
         // TODO(zachh): There's a problem here if a contact for a new row is deleted?
@@ -476,20 +559,7 @@
     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(
+  private static Set<DialerPhoneNumber> findDialerPhoneNumbersContainingContactId(
       ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> existingInfoMap, long contactId) {
     Set<DialerPhoneNumber> matches = new ArraySet<>();
     for (Entry<DialerPhoneNumber, PhoneLookupInfo> entry : existingInfoMap.entrySet()) {
@@ -514,4 +584,56 @@
     }
     return where.toString();
   }
+
+  /**
+   * Divides a set of {@link DialerPhoneNumber DialerPhoneNumbers} by those that can be formatted to
+   * E164 and those that cannot.
+   */
+  private static class PartitionedNumbers {
+    private Map<String, Set<DialerPhoneNumber>> e164NumbersToDialerPhoneNumbers = new ArrayMap<>();
+    private Map<String, Set<DialerPhoneNumber>> unformattableNumbersToDialerPhoneNumbers =
+        new ArrayMap<>();
+
+    PartitionedNumbers(Set<DialerPhoneNumber> dialerPhoneNumbers) {
+      DialerPhoneNumberUtil dialerPhoneNumberUtil =
+          new DialerPhoneNumberUtil(PhoneNumberUtil.getInstance());
+      for (DialerPhoneNumber dialerPhoneNumber : dialerPhoneNumbers) {
+        Optional<String> e164 = dialerPhoneNumberUtil.formatToE164(dialerPhoneNumber);
+        if (e164.isPresent()) {
+          String validE164 = e164.get();
+          Set<DialerPhoneNumber> currentNumbers = e164NumbersToDialerPhoneNumbers.get(validE164);
+          if (currentNumbers == null) {
+            currentNumbers = new ArraySet<>();
+            e164NumbersToDialerPhoneNumbers.put(validE164, currentNumbers);
+          }
+          currentNumbers.add(dialerPhoneNumber);
+        } else {
+          String unformattableNumber = dialerPhoneNumber.getRawInput().getNumber();
+          Set<DialerPhoneNumber> currentNumbers =
+              unformattableNumbersToDialerPhoneNumbers.get(unformattableNumber);
+          if (currentNumbers == null) {
+            currentNumbers = new ArraySet<>();
+            unformattableNumbersToDialerPhoneNumbers.put(unformattableNumber, currentNumbers);
+          }
+          currentNumbers.add(dialerPhoneNumber);
+        }
+      }
+    }
+
+    Set<String> unformattableNumbers() {
+      return unformattableNumbersToDialerPhoneNumbers.keySet();
+    }
+
+    Set<String> validE164Numbers() {
+      return e164NumbersToDialerPhoneNumbers.keySet();
+    }
+
+    Set<DialerPhoneNumber> dialerPhoneNumbersForE164(String e164) {
+      return e164NumbersToDialerPhoneNumbers.get(e164);
+    }
+
+    Set<DialerPhoneNumber> dialerPhoneNumbersForUnformattable(String unformattableNumber) {
+      return unformattableNumbersToDialerPhoneNumbers.get(unformattableNumber);
+    }
+  }
 }
diff --git a/java/com/android/dialer/phonenumbercache/ContactInfoHelper.java b/java/com/android/dialer/phonenumbercache/ContactInfoHelper.java
index 8e08fb2..39e3866 100644
--- a/java/com/android/dialer/phonenumbercache/ContactInfoHelper.java
+++ b/java/com/android/dialer/phonenumbercache/ContactInfoHelper.java
@@ -340,7 +340,12 @@
     try (Cursor phoneLookupCursor =
         mContext
             .getContentResolver()
-            .query(uri, PhoneQuery.getPhoneLookupProjection(uri), null, null, null)) {
+            .query(
+                uri,
+                PhoneQuery.getPhoneLookupProjection(uri),
+                null /* selection */,
+                null /* selectionArgs */,
+                null /* sortOrder */)) {
       if (phoneLookupCursor == null) {
         LogUtil.d("ContactInfoHelper.lookupContactFromUri", "phoneLookupCursor is null");
         return null;
@@ -350,15 +355,8 @@
         return ContactInfo.EMPTY;
       }
 
-      Cursor matchedCursor =
-          PhoneNumberHelper.getCursorMatchForContactLookupUri(
-              phoneLookupCursor, PhoneQuery.MATCHED_NUMBER, uri);
-      if (matchedCursor == null) {
-        return ContactInfo.EMPTY;
-      }
-
-      String lookupKey = matchedCursor.getString(PhoneQuery.LOOKUP_KEY);
-      ContactInfo contactInfo = createPhoneLookupContactInfo(matchedCursor, lookupKey);
+      String lookupKey = phoneLookupCursor.getString(PhoneQuery.LOOKUP_KEY);
+      ContactInfo contactInfo = createPhoneLookupContactInfo(phoneLookupCursor, lookupKey);
       fillAdditionalContactInfo(mContext, contactInfo);
       return contactInfo;
     }
diff --git a/java/com/android/dialer/phonenumberproto/DialerPhoneNumberUtil.java b/java/com/android/dialer/phonenumberproto/DialerPhoneNumberUtil.java
index ac011d4..d23b5a1 100644
--- a/java/com/android/dialer/phonenumberproto/DialerPhoneNumberUtil.java
+++ b/java/com/android/dialer/phonenumberproto/DialerPhoneNumberUtil.java
@@ -25,6 +25,7 @@
 import com.android.dialer.DialerPhoneNumber.RawInput;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
+import com.google.common.base.Optional;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.i18n.phonenumbers.NumberParseException;
@@ -127,17 +128,30 @@
   }
 
   /**
-   * Formats the provided number to e164 format. May return raw number if number is unparseable.
+   * Formats the provided number to e164 format or return raw number if number is unparseable.
    *
    * @see PhoneNumberUtil#format(PhoneNumber, PhoneNumberFormat)
    */
   @WorkerThread
-  public String formatToE164(@NonNull DialerPhoneNumber number) {
+  public String normalizeNumber(DialerPhoneNumber number) {
+    Assert.isWorkerThread();
+    return formatToE164(number).or(number.getRawInput().getNumber());
+  }
+
+  /**
+   * Formats the provided number to e164 format if possible.
+   *
+   * @see PhoneNumberUtil#format(PhoneNumber, PhoneNumberFormat)
+   */
+  @WorkerThread
+  public Optional<String> formatToE164(DialerPhoneNumber number) {
     Assert.isWorkerThread();
     if (number.hasDialerInternalPhoneNumber()) {
-      return phoneNumberUtil.format(
-          Converter.protoToPojo(number.getDialerInternalPhoneNumber()), PhoneNumberFormat.E164);
+      return Optional.of(
+          phoneNumberUtil.format(
+              Converter.protoToPojo(number.getDialerInternalPhoneNumber()),
+              PhoneNumberFormat.E164));
     }
-    return number.getRawInput().getNumber();
+    return Optional.absent();
   }
 }
diff --git a/java/com/android/dialer/phonenumberutil/PhoneNumberHelper.java b/java/com/android/dialer/phonenumberutil/PhoneNumberHelper.java
index e32ace5..be1b062 100644
--- a/java/com/android/dialer/phonenumberutil/PhoneNumberHelper.java
+++ b/java/com/android/dialer/phonenumberutil/PhoneNumberHelper.java
@@ -17,8 +17,6 @@
 package com.android.dialer.phonenumberutil;
 
 import android.content.Context;
-import android.database.Cursor;
-import android.net.Uri;
 import android.os.Trace;
 import android.provider.CallLog;
 import android.support.annotation.NonNull;
@@ -29,7 +27,6 @@
 import android.text.BidiFormatter;
 import android.text.TextDirectionHeuristics;
 import android.text.TextUtils;
-import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.compat.CompatUtils;
 import com.android.dialer.compat.telephony.TelephonyManagerCompat;
@@ -53,78 +50,6 @@
   }
 
   /**
-   * Find the cursor pointing to a number that matches the number in a contact lookup URI.
-   *
-   * <p>When determining whether two phone numbers are identical enough for caller ID purposes, the
-   * Contacts Provider uses {@link PhoneNumberUtils#compare(String, String)}, which ignores special
-   * dialable characters such as '#', '*', '+', etc. This makes it possible for the cursor returned
-   * by the Contacts Provider to have multiple rows even when the URI asks for a specific number.
-   *
-   * <p>For example, suppose the user has two contacts whose numbers are "#123" and "123",
-   * respectively. When the URI asks for number "123", both numbers will be returned. Therefore, the
-   * following strategy is employed to find a match.
-   *
-   * <p>If the cursor points to a global phone number (i.e., a number that can be accepted by {@link
-   * PhoneNumberUtils#isGlobalPhoneNumber(String)}) and the lookup number in the URI is a PARTIAL
-   * match, return the cursor.
-   *
-   * <p>If the cursor points to a number that is not a global phone number, return the cursor iff
-   * the lookup number in the URI is an EXACT match.
-   *
-   * <p>Return null in all other circumstances.
-   *
-   * @param cursor A cursor returned by the Contacts Provider.
-   * @param columnIndexForNumber The index of the column where phone numbers are stored. It is the
-   *     caller's responsibility to pass the correct column index.
-   * @param contactLookupUri A URI used to retrieve a contact via the Contacts Provider. It is the
-   *     caller's responsibility to ensure the URI is one that asks for a specific phone number.
-   * @return The cursor considered as a match by the description above or null if no such cursor can
-   *     be found.
-   */
-  public static Cursor getCursorMatchForContactLookupUri(
-      Cursor cursor, int columnIndexForNumber, Uri contactLookupUri) {
-    if (cursor == null || contactLookupUri == null) {
-      return null;
-    }
-
-    if (!cursor.moveToFirst()) {
-      return null;
-    }
-
-    Assert.checkArgument(
-        0 <= columnIndexForNumber && columnIndexForNumber < cursor.getColumnCount());
-
-    String lookupNumber = contactLookupUri.getLastPathSegment();
-    if (lookupNumber == null) {
-      return null;
-    }
-
-    boolean isMatchFound;
-    do {
-      // All undialable characters should be converted/removed before comparing the lookup number
-      // and the existing contact number.
-      String rawExistingContactNumber =
-          PhoneNumberUtils.stripSeparators(
-              PhoneNumberUtils.convertKeypadLettersToDigits(
-                  cursor.getString(columnIndexForNumber)));
-      String rawQueryNumber =
-          PhoneNumberUtils.stripSeparators(
-              PhoneNumberUtils.convertKeypadLettersToDigits(lookupNumber));
-
-      isMatchFound =
-          PhoneNumberUtils.isGlobalPhoneNumber(rawExistingContactNumber)
-              ? rawExistingContactNumber.contains(rawQueryNumber)
-              : rawExistingContactNumber.equals(rawQueryNumber);
-
-      if (isMatchFound) {
-        return cursor;
-      }
-    } while (cursor.moveToNext());
-
-    return null;
-  }
-
-  /**
    * Returns true if the given number is the number of the configured voicemail. To be able to
    * mock-out this, it is not a static method.
    */
diff --git a/java/com/android/dialer/speeddial/DisambigDialog.java b/java/com/android/dialer/speeddial/DisambigDialog.java
new file mode 100644
index 0000000..ca02f41
--- /dev/null
+++ b/java/com/android/dialer/speeddial/DisambigDialog.java
@@ -0,0 +1,244 @@
+/*
+ * 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.speeddial;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.FragmentManager;
+import android.content.ContentResolver;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import com.android.dialer.callintent.CallInitiationType;
+import com.android.dialer.callintent.CallIntentBuilder;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.concurrent.DialerExecutor.Worker;
+import com.android.dialer.common.concurrent.DialerExecutorComponent;
+import com.android.dialer.duo.DuoComponent;
+import com.android.dialer.precall.PreCall;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Set;
+
+/** Disambiguation dialog for favorite contacts in {@link SpeedDialFragment}. */
+public class DisambigDialog extends DialogFragment {
+
+  @VisibleForTesting public static final String DISAMBIG_DIALOG_TAG = "disambig_dialog";
+  private static final String DISAMBIG_DIALOG_WORKER_TAG = "disambig_dialog_worker";
+
+  private final Set<String> phoneNumbers = new ArraySet<>();
+  private LinearLayout container;
+  private String lookupKey;
+
+  /** Show a disambiguation dialog for a starred contact without a favorite communication avenue. */
+  public static DisambigDialog show(String lookupKey, FragmentManager manager) {
+    DisambigDialog dialog = new DisambigDialog();
+    dialog.lookupKey = lookupKey;
+    dialog.show(manager, DISAMBIG_DIALOG_TAG);
+    return dialog;
+  }
+
+  @Override
+  public Dialog onCreateDialog(Bundle savedInstanceState) {
+    LayoutInflater inflater = getActivity().getLayoutInflater();
+    View view = inflater.inflate(R.layout.disambig_dialog_layout, null, false);
+    container = view.findViewById(R.id.communication_avenue_container);
+    return new AlertDialog.Builder(getActivity()).setView(view).create();
+  }
+
+  @Override
+  public void onResume() {
+    super.onResume();
+    lookupContactInfo();
+  }
+
+  @Override
+  public void onPause() {
+    super.onPause();
+    // TODO(calderwoodra): for simplicity, just dismiss the dialog on configuration change and
+    // consider changing this later.
+    dismiss();
+  }
+
+  private void lookupContactInfo() {
+    DialerExecutorComponent.get(getContext())
+        .dialerExecutorFactory()
+        .createUiTaskBuilder(
+            getFragmentManager(),
+            DISAMBIG_DIALOG_WORKER_TAG,
+            new LookupContactInfoWorker(getContext().getContentResolver()))
+        .onSuccess(this::insertOptions)
+        .onFailure(this::onLookupFailed)
+        .build()
+        .executeParallel(lookupKey);
+  }
+
+  /**
+   * Inflates and inserts the following in the dialog:
+   *
+   * <ul>
+   *   <li>Header for each unique phone number
+   *   <li>Clickable video option if the phone number is video reachable (ViLTE, Duo)
+   *   <li>Clickable voice option
+   * </ul>
+   */
+  private void insertOptions(Cursor cursor) {
+    if (!cursorIsValid(cursor)) {
+      dismiss();
+      return;
+    }
+
+    do {
+      String number = cursor.getString(LookupContactInfoWorker.NUMBER_INDEX);
+      // TODO(calderwoodra): improve this to include fuzzy matching
+      if (phoneNumbers.add(number)) {
+        insertOption(
+            number,
+            getLabel(getContext().getResources(), cursor),
+            isVideoReachable(cursor, number));
+      }
+    } while (cursor.moveToNext());
+    cursor.close();
+    // TODO(calderwoodra): set max height of the scrollview. Might need to override onMeasure.
+  }
+
+  /** Returns true if the given number is ViLTE reachable or Duo reachable. */
+  private boolean isVideoReachable(Cursor cursor, String number) {
+    boolean isVideoReachable = cursor.getInt(LookupContactInfoWorker.PHONE_PRESENCE_INDEX) == 1;
+    if (!isVideoReachable) {
+      isVideoReachable = DuoComponent.get(getContext()).getDuo().isReachable(getContext(), number);
+    }
+    return isVideoReachable;
+  }
+
+  /** Inserts a group of options for a specific phone number. */
+  private void insertOption(String number, String phoneType, boolean isVideoReachable) {
+    View view =
+        getActivity()
+            .getLayoutInflater()
+            .inflate(R.layout.disambig_option_layout, container, false);
+    ((TextView) view.findViewById(R.id.phone_type)).setText(phoneType);
+    ((TextView) view.findViewById(R.id.phone_number)).setText(number);
+
+    if (isVideoReachable) {
+      View videoOption = view.findViewById(R.id.video_call_container);
+      videoOption.setOnClickListener(v -> onVideoOptionClicked(number));
+      videoOption.setVisibility(View.VISIBLE);
+    }
+    View voiceOption = view.findViewById(R.id.voice_call_container);
+    voiceOption.setOnClickListener(v -> onVoiceOptionClicked(number));
+    container.addView(view);
+  }
+
+  private void onVideoOptionClicked(String number) {
+    // TODO(calderwoodra): save this option if remember is checked
+    // TODO(calderwoodra): place a duo call if possible
+    PreCall.start(
+        getContext(),
+        new CallIntentBuilder(number, CallInitiationType.Type.SPEED_DIAL).setIsVideoCall(true));
+  }
+
+  private void onVoiceOptionClicked(String number) {
+    // TODO(calderwoodra): save this option if remember is checked
+    PreCall.start(getContext(), new CallIntentBuilder(number, CallInitiationType.Type.SPEED_DIAL));
+  }
+
+  // TODO(calderwoodra): handle CNAP and cequint types.
+  // TODO(calderwoodra): unify this into a utility method with CallLogAdapter#getNumberType
+  private static String getLabel(Resources resources, Cursor cursor) {
+    int numberType = cursor.getInt(LookupContactInfoWorker.PHONE_TYPE_INDEX);
+    String numberLabel = cursor.getString(LookupContactInfoWorker.PHONE_LABEL_INDEX);
+
+    // Returns empty label instead of "custom" if the custom label is empty.
+    if (numberType == Phone.TYPE_CUSTOM && TextUtils.isEmpty(numberLabel)) {
+      return "";
+    }
+    return (String) Phone.getTypeLabel(resources, numberType, numberLabel);
+  }
+
+  // Checks if the cursor is valid and logs an error if there are any issues.
+  private static boolean cursorIsValid(Cursor cursor) {
+    if (cursor == null) {
+      LogUtil.e("DisambigDialog.insertOptions", "cursor null.");
+      return false;
+    } else if (cursor.isClosed()) {
+      LogUtil.e("DisambigDialog.insertOptions", "cursor closed.");
+      cursor.close();
+      return false;
+    } else if (!cursor.moveToFirst()) {
+      LogUtil.e("DisambigDialog.insertOptions", "cursor empty.");
+      cursor.close();
+      return false;
+    }
+    return true;
+  }
+
+  private void onLookupFailed(Throwable throwable) {
+    LogUtil.e("DisambigDialog.onLookupFailed", null, throwable);
+    insertOptions(null);
+  }
+
+  private static class LookupContactInfoWorker implements Worker<String, Cursor> {
+
+    static final int NUMBER_INDEX = 0;
+    static final int PHONE_TYPE_INDEX = 1;
+    static final int PHONE_LABEL_INDEX = 2;
+    static final int PHONE_PRESENCE_INDEX = 3;
+
+    private static final String[] projection =
+        new String[] {Phone.NUMBER, Phone.TYPE, Phone.LABEL, Phone.CARRIER_PRESENCE};
+    private final ContentResolver resolver;
+
+    LookupContactInfoWorker(ContentResolver resolver) {
+      this.resolver = resolver;
+    }
+
+    @Nullable
+    @Override
+    public Cursor doInBackground(@Nullable String lookupKey) throws Throwable {
+      if (TextUtils.isEmpty(lookupKey)) {
+        LogUtil.e("LookupConctactInfoWorker.doInBackground", "contact id unsest.");
+        return null;
+      }
+      return resolver.query(
+          Phone.CONTENT_URI, projection, Phone.LOOKUP_KEY + " = ?", new String[] {lookupKey}, null);
+    }
+  }
+
+  @VisibleForTesting
+  public static String[] getProjectionForTesting() {
+    ArrayList<String> projection =
+        new ArrayList<>(Arrays.asList(LookupContactInfoWorker.projection));
+    projection.add(Phone.LOOKUP_KEY);
+    return projection.toArray(new String[projection.size()]);
+  }
+
+  @VisibleForTesting
+  public LinearLayout getContainer() {
+    return container;
+  }
+}
diff --git a/java/com/android/dialer/speeddial/FavoritesViewHolder.java b/java/com/android/dialer/speeddial/FavoritesViewHolder.java
index 0cde716..c25b05e 100644
--- a/java/com/android/dialer/speeddial/FavoritesViewHolder.java
+++ b/java/com/android/dialer/speeddial/FavoritesViewHolder.java
@@ -45,8 +45,10 @@
   private final TextView phoneType;
   private final FrameLayout videoCallIcon;
 
+  private boolean hasDefaultNumber;
   private boolean isVideoCall;
   private String number;
+  private String lookupKey;
 
   public FavoritesViewHolder(View view, FavoriteContactsListener listener) {
     super(view);
@@ -67,7 +69,7 @@
 
     String name = cursor.getString(StrequentContactsCursorLoader.PHONE_DISPLAY_NAME);
     long contactId = cursor.getLong(StrequentContactsCursorLoader.PHONE_ID);
-    String lookupKey = cursor.getString(StrequentContactsCursorLoader.PHONE_LOOKUP_KEY);
+    lookupKey = cursor.getString(StrequentContactsCursorLoader.PHONE_LOOKUP_KEY);
     Uri contactUri = Contacts.getLookupUri(contactId, lookupKey);
 
     String photoUri = cursor.getString(StrequentContactsCursorLoader.PHONE_PHOTO_URI);
@@ -82,6 +84,9 @@
     nameView.setText(name);
     phoneType.setText(getLabel(context.getResources(), cursor));
     videoCallIcon.setVisibility(isVideoCall ? View.VISIBLE : View.GONE);
+
+    // TODO(calderwoodra): Update this to include communication avenues also
+    hasDefaultNumber = cursor.getInt(StrequentContactsCursorLoader.PHONE_IS_SUPER_PRIMARY) != 0;
   }
 
   // TODO(calderwoodra): handle CNAP and cequint types.
@@ -99,7 +104,11 @@
 
   @Override
   public void onClick(View v) {
-    listener.onClick(number, isVideoCall);
+    if (hasDefaultNumber) {
+      listener.onClick(number, isVideoCall);
+    } else {
+      listener.onAmbiguousContactClicked(lookupKey);
+    }
   }
 
   @Override
@@ -112,6 +121,9 @@
   /** Listener/callback for {@link FavoritesViewHolder} actions. */
   public interface FavoriteContactsListener {
 
+    /** Called when the user clicks on a favorite contact that doesn't have a default number. */
+    void onAmbiguousContactClicked(String contactId);
+
     /** Called when the user clicks on a favorite contact. */
     void onClick(String number, boolean isVideoCall);
 
diff --git a/java/com/android/dialer/speeddial/SpeedDialFragment.java b/java/com/android/dialer/speeddial/SpeedDialFragment.java
index 08861da..979c894 100644
--- a/java/com/android/dialer/speeddial/SpeedDialFragment.java
+++ b/java/com/android/dialer/speeddial/SpeedDialFragment.java
@@ -98,6 +98,11 @@
   private class SpeedDialFavoritesListener implements FavoriteContactsListener {
 
     @Override
+    public void onAmbiguousContactClicked(String lookupKey) {
+      DisambigDialog.show(lookupKey, getFragmentManager());
+    }
+
+    @Override
     public void onClick(String number, boolean isVideoCall) {
       // TODO(calderwoodra): add logic for duo video calls
       PreCall.start(
diff --git a/java/com/android/dialer/speeddial/StrequentContactsCursorLoader.java b/java/com/android/dialer/speeddial/StrequentContactsCursorLoader.java
index f5f0045..e9e3e32 100644
--- a/java/com/android/dialer/speeddial/StrequentContactsCursorLoader.java
+++ b/java/com/android/dialer/speeddial/StrequentContactsCursorLoader.java
@@ -18,7 +18,6 @@
 
 import android.content.Context;
 import android.content.CursorLoader;
-import android.database.ContentObserver;
 import android.database.Cursor;
 import android.database.MatrixCursor;
 import android.net.Uri;
@@ -58,8 +57,6 @@
         Phone.CONTACT_ID, // 11
       };
 
-  private final ContentObserver contentObserver = new ForceLoadContentObserver();
-
   StrequentContactsCursorLoader(Context context) {
     super(
         context,
@@ -97,8 +94,4 @@
   public Cursor loadInBackground() {
     return SpeedDialCursor.newInstance(super.loadInBackground());
   }
-
-  ContentObserver getContentObserver() {
-    return contentObserver;
-  }
 }
diff --git a/java/com/android/dialer/speeddial/res/layout/disambig_dialog_layout.xml b/java/com/android/dialer/speeddial/res/layout/disambig_dialog_layout.xml
new file mode 100644
index 0000000..3562058
--- /dev/null
+++ b/java/com/android/dialer/speeddial/res/layout/disambig_dialog_layout.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+  <TextView
+      android:id="@+id/disambig_dialog_title"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:gravity="center_vertical"
+      android:minHeight="64dp"
+      android:paddingStart="24dp"
+      android:paddingEnd="24dp"
+      android:elevation="1dp"
+      android:text="@string/speed_dial_disambig_dialog_title"
+      android:textSize="20sp"
+      android:textColor="@color/dialer_primary_text_color"
+      android:fontFamily="sans-serif-medium"
+      android:background="@android:color/white"/>
+
+  <ScrollView
+      android:id="@+id/disambig_scrollview"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content">
+
+    <LinearLayout
+        android:id="@+id/communication_avenue_container"
+        android:orientation="vertical"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"/>
+  </ScrollView>
+
+  <FrameLayout
+      android:id="@+id/remember_this_choice_container"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:layout_gravity="bottom"
+      android:minHeight="56dp"
+      android:padding="12dp"
+      android:elevation="4dp"
+      android:background="@android:color/white">
+
+    <CheckBox
+        android:id="@+id/remember_this_choice_checkbox"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/speed_dial_remember_this_choice"/>
+  </FrameLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/java/com/android/dialer/speeddial/res/layout/disambig_option_layout.xml b/java/com/android/dialer/speeddial/res/layout/disambig_option_layout.xml
new file mode 100644
index 0000000..097ac40
--- /dev/null
+++ b/java/com/android/dialer/speeddial/res/layout/disambig_option_layout.xml
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/disambig_option_container"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:minHeight="56dp"
+    android:layout_marginBottom="8dp">
+
+  <LinearLayout
+      android:orientation="vertical"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:minHeight="56dp"
+      android:gravity="center_vertical"
+      android:paddingStart="24dp"
+      android:paddingEnd="24dp">
+
+    <TextView
+        android:id="@+id/phone_type"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        style="@style/PrimaryText"/>
+
+    <TextView
+        android:id="@+id/phone_number"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        style="@style/SecondaryText"/>
+  </LinearLayout>
+
+  <LinearLayout
+      android:id="@+id/video_call_container"
+      android:orientation="horizontal"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:paddingStart="24dp"
+      android:paddingEnd="24dp"
+      android:minHeight="56dp"
+      android:background="?android:attr/selectableItemBackground"
+      android:visibility="gone"
+      android:contentDescription="@string/disambig_option_video_call">
+
+    <ImageView
+        android:layout_width="24dp"
+        android:layout_height="24dp"
+        android:layout_gravity="center_vertical"
+        android:tint="@color/dialer_secondary_text_color"
+        android:src="@drawable/quantum_ic_videocam_vd_theme_24"/>
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="12dp"
+        android:layout_gravity="center_vertical"
+        android:text="@string/disambig_option_video_call"
+        style="@style/PrimaryText"/>
+  </LinearLayout>
+
+  <LinearLayout
+      android:id="@+id/voice_call_container"
+      android:orientation="horizontal"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:paddingStart="24dp"
+      android:paddingEnd="24dp"
+      android:minHeight="56dp"
+      android:background="?android:attr/selectableItemBackground"
+      android:contentDescription="@string/disambig_option_voice_call">
+
+    <ImageView
+        android:id="@+id/disambig_option_icon"
+        android:layout_width="24dp"
+        android:layout_height="24dp"
+        android:layout_gravity="center_vertical"
+        android:tint="@color/dialer_secondary_text_color"
+        android:src="@drawable/quantum_ic_phone_vd_theme_24"/>
+
+    <TextView
+        android:id="@+id/disambig_option_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="12dp"
+        android:layout_gravity="center_vertical"
+        android:text="@string/disambig_option_voice_call"
+        style="@style/PrimaryText"/>
+  </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/java/com/android/dialer/speeddial/res/values/dimens.xml b/java/com/android/dialer/speeddial/res/values/dimens.xml
index 5929df8..74b509b 100644
--- a/java/com/android/dialer/speeddial/res/values/dimens.xml
+++ b/java/com/android/dialer/speeddial/res/values/dimens.xml
@@ -15,4 +15,5 @@
   ~ limitations under the License
   -->
 <resources>
+  <dimen name="scrollview_max_height">280dp</dimen>
 </resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/speeddial/res/values/strings.xml b/java/com/android/dialer/speeddial/res/values/strings.xml
index d64d035..677f772 100644
--- a/java/com/android/dialer/speeddial/res/values/strings.xml
+++ b/java/com/android/dialer/speeddial/res/values/strings.xml
@@ -24,6 +24,20 @@
   <!-- text for a button that prompts the user to add a contact to their favorites. [CHAR LIMIT=12] -->
   <string name="speed_dial_add_button_text">Add</string>
 
+  <!-- text for a checkbox in a dialog that prompts the user to select a phone number from a list.
+    If the user checks this box, we will remember their selection and never ask for it again. [CHAR LIMIT=NONE]-->
+  <string name="speed_dial_remember_this_choice">Remember this choice</string>
+
+  <!-- Title of a dialog asking the user to choose their favorite mode of communication for a
+    specific contact where communication modes are video calling and voice calling. [CHAR LIMIT=NONE]-->
+  <string name="speed_dial_disambig_dialog_title">Choose a Favorite mode</string>
+
+  <!-- Text for a button that places a video call [CHAR LIMIT=15]-->
+  <string name="disambig_option_video_call">Video call</string>
+
+  <!-- Text for a button that places a phone/voice call [CHAR LIMIT=15]-->
+  <string name="disambig_option_voice_call">Call</string>
+
   <!-- Title for screen prompting the user to select a contact to mark as a favorite. [CHAR LIMIT=NONE] -->
   <string name="add_favorite_activity_title">Add Favorite</string>
 </resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/telecom/TelecomCallUtil.java b/java/com/android/dialer/telecom/TelecomCallUtil.java
new file mode 100644
index 0000000..acec498
--- /dev/null
+++ b/java/com/android/dialer/telecom/TelecomCallUtil.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2015 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.telecom;
+
+import android.content.Context;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.WorkerThread;
+import android.telecom.Call;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.SubscriptionInfo;
+import android.text.TextUtils;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.google.common.base.Optional;
+import java.util.Locale;
+
+/**
+ * Class to provide a standard interface for obtaining information from the underlying
+ * android.telecom.Call. Much of this should be obtained through the incall.Call, but on occasion we
+ * need to interact with the telecom.Call directly (eg. call blocking, before the incall.Call has
+ * been created).
+ */
+public class TelecomCallUtil {
+
+  /** Returns Whether the call handle is an emergency number. */
+  public static boolean isEmergencyCall(@NonNull Call call) {
+    Assert.isNotNull(call);
+    Uri handle = call.getDetails().getHandle();
+    return PhoneNumberUtils.isEmergencyNumber(handle == null ? "" : handle.getSchemeSpecificPart());
+  }
+
+  /**
+   * Returns The phone number which the {@code Call} is currently connected, or {@code null} if the
+   * number is not available.
+   */
+  @Nullable
+  public static String getNumber(@Nullable Call call) {
+    if (call == null) {
+      return null;
+    }
+    if (call.getDetails().getGatewayInfo() != null) {
+      return call.getDetails().getGatewayInfo().getOriginalAddress().getSchemeSpecificPart();
+    }
+    Uri handle = getHandle(call);
+    return handle == null ? null : handle.getSchemeSpecificPart();
+  }
+
+  /**
+   * Returns The handle (e.g., phone number) to which the {@code Call} is currently connected, or
+   * {@code null} if the number is not available.
+   */
+  @Nullable
+  public static Uri getHandle(@Nullable Call call) {
+    return call == null ? null : call.getDetails().getHandle();
+  }
+
+  /**
+   * Normalizes the number of the {@code call} to E.164. If the country code is missing in the
+   * number the SIM's country will be used. Only removes non-dialable digits if the country code is
+   * missing.
+   */
+  @WorkerThread
+  public static Optional<String> getNormalizedNumber(Context appContext, Call call) {
+    Assert.isWorkerThread();
+    PhoneAccountHandle phoneAccountHandle = call.getDetails().getAccountHandle();
+    Optional<SubscriptionInfo> subscriptionInfo =
+        TelecomUtil.getSubscriptionInfo(appContext, phoneAccountHandle);
+    String rawNumber = getNumber(call);
+    if (TextUtils.isEmpty(rawNumber)) {
+      return Optional.absent();
+    }
+    String normalizedNumber = PhoneNumberUtils.normalizeNumber(rawNumber);
+    if (TextUtils.isEmpty(normalizedNumber)) {
+      return Optional.absent();
+    }
+    String countryCode =
+        subscriptionInfo.isPresent() ? subscriptionInfo.get().getCountryIso() : null;
+    if (countryCode == null) {
+      LogUtil.w(
+          "PhoneLookupHistoryRecorder.getNormalizedNumber",
+          "couldn't find a country code for call");
+      return Optional.of(normalizedNumber);
+    }
+
+    String e164Number =
+        PhoneNumberUtils.formatNumberToE164(rawNumber, countryCode.toUpperCase(Locale.US));
+    return e164Number == null ? Optional.of(normalizedNumber) : Optional.of(e164Number);
+  }
+}
diff --git a/java/com/android/incallui/CallerInfo.java b/java/com/android/incallui/CallerInfo.java
index 4a9cf21..5c43b4f 100644
--- a/java/com/android/incallui/CallerInfo.java
+++ b/java/com/android/incallui/CallerInfo.java
@@ -192,7 +192,7 @@
    *
    * @param context the context used to retrieve string constants
    * @param contactRef the URI to attach to this CallerInfo object
-   * @param cursor the first matching object in the cursor is used to build the CallerInfo object.
+   * @param cursor the first object in the cursor is used to build the CallerInfo object.
    * @return the CallerInfo which contains the caller id for the given number. The returned
    *     CallerInfo is null if no number is supplied.
    */
@@ -223,24 +223,18 @@
     long contactId = 0L;
     int columnIndex;
 
-    // If the cursor has the phone number column, find the one that matches the lookup number in the
-    // URI.
-    columnIndex = cursor.getColumnIndex(PhoneLookup.NUMBER);
-    if (columnIndex != -1 && contactRef != null) {
-      cursor = PhoneNumberHelper.getCursorMatchForContactLookupUri(cursor, columnIndex, contactRef);
-      if (cursor != null) {
-        info.phoneNumber = cursor.getString(columnIndex);
-      } else {
-        return info;
-      }
-    }
-
     // Look for the name
     columnIndex = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME);
     if (columnIndex != -1) {
       info.name = normalize(cursor.getString(columnIndex));
     }
 
+    // Look for the number
+    columnIndex = cursor.getColumnIndex(PhoneLookup.NUMBER);
+    if (columnIndex != -1) {
+      info.phoneNumber = cursor.getString(columnIndex);
+    }
+
     // Look for the normalized number
     columnIndex = cursor.getColumnIndex(PhoneLookup.NORMALIZED_NUMBER);
     if (columnIndex != -1) {
@@ -510,7 +504,6 @@
       Log.e(TAG, "Cannot access VoiceMail.", se);
     }
     // TODO: There is no voicemail picture?
-
     // photoResource = android.R.drawable.badge_voicemail;
     return this;
   }
diff --git a/java/com/android/incallui/DialpadFragment.java b/java/com/android/incallui/DialpadFragment.java
index 2f3a68c..b2aacf7 100644
--- a/java/com/android/incallui/DialpadFragment.java
+++ b/java/com/android/incallui/DialpadFragment.java
@@ -202,15 +202,6 @@
     mDtmfDialerField.setText(PhoneNumberUtilsCompat.createTtsSpannable(text));
   }
 
-  @Override
-  public void setVisible(boolean on) {
-    if (on) {
-      getView().setVisibility(View.VISIBLE);
-    } else {
-      getView().setVisibility(View.INVISIBLE);
-    }
-  }
-
   /** Starts the slide up animation for the Dialpad keys when the Dialpad is revealed. */
   public void animateShowDialpad() {
     final DialpadView dialpadView = (DialpadView) getView().findViewById(R.id.dialpad_view);
diff --git a/java/com/android/incallui/DialpadPresenter.java b/java/com/android/incallui/DialpadPresenter.java
index 7a784c2..002fefc 100644
--- a/java/com/android/incallui/DialpadPresenter.java
+++ b/java/com/android/incallui/DialpadPresenter.java
@@ -84,8 +84,6 @@
 
   public interface DialpadUi extends Ui {
 
-    void setVisible(boolean on);
-
     void appendDigitsToField(char digit);
   }
 }
diff --git a/java/com/android/incallui/ExternalCallNotifier.java b/java/com/android/incallui/ExternalCallNotifier.java
index 9e78052..7915b85 100644
--- a/java/com/android/incallui/ExternalCallNotifier.java
+++ b/java/com/android/incallui/ExternalCallNotifier.java
@@ -44,11 +44,11 @@
 import com.android.dialer.contactphoto.BitmapUtil;
 import com.android.dialer.notification.DialerNotificationManager;
 import com.android.dialer.notification.NotificationChannelId;
+import com.android.dialer.telecom.TelecomCallUtil;
 import com.android.incallui.call.DialerCall;
 import com.android.incallui.call.DialerCallDelegate;
 import com.android.incallui.call.ExternalCallList;
 import com.android.incallui.latencyreport.LatencyReport;
-import com.android.incallui.util.TelecomCallUtil;
 import java.util.Map;
 
 /**
diff --git a/java/com/android/incallui/InCallActivity.java b/java/com/android/incallui/InCallActivity.java
index 28ff7da..47b5986 100644
--- a/java/com/android/incallui/InCallActivity.java
+++ b/java/com/android/incallui/InCallActivity.java
@@ -35,7 +35,6 @@
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.annotation.VisibleForTesting;
-import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentManager;
 import android.support.v4.app.FragmentTransaction;
 import android.support.v4.content.res.ResourcesCompat;
@@ -787,6 +786,7 @@
       transaction.add(getDialpadContainerId(), new DialpadFragment(), Tags.DIALPAD_FRAGMENT);
     } else {
       transaction.show(dialpadFragment);
+      dialpadFragment.setUserVisibleHint(true);
     }
     transaction.commitAllowingStateLoss();
     dialpadFragmentManager.executePendingTransactions();
@@ -801,19 +801,20 @@
       return;
     }
 
-    Fragment dialpadFragment = dialpadFragmentManager.findFragmentByTag(Tags.DIALPAD_FRAGMENT);
+    DialpadFragment dialpadFragment = getDialpadFragment();
     if (dialpadFragment != null) {
       FragmentTransaction transaction = dialpadFragmentManager.beginTransaction();
       transaction.hide(dialpadFragment);
       transaction.commitAllowingStateLoss();
       dialpadFragmentManager.executePendingTransactions();
+      dialpadFragment.setUserVisibleHint(false);
     }
     updateNavigationBar(false /* isDialpadVisible */);
   }
 
   public boolean isDialpadVisible() {
     DialpadFragment dialpadFragment = getDialpadFragment();
-    return dialpadFragment != null && dialpadFragment.isVisible();
+    return dialpadFragment != null && dialpadFragment.getUserVisibleHint();
   }
 
   /** Returns the {@link DialpadFragment} that's shown by this activity, or {@code null} */
diff --git a/java/com/android/incallui/InCallPresenter.java b/java/com/android/incallui/InCallPresenter.java
index f8605ae..3debd70 100644
--- a/java/com/android/incallui/InCallPresenter.java
+++ b/java/com/android/incallui/InCallPresenter.java
@@ -52,6 +52,7 @@
 import com.android.dialer.logging.InteractionEvent;
 import com.android.dialer.logging.Logger;
 import com.android.dialer.postcall.PostCall;
+import com.android.dialer.telecom.TelecomCallUtil;
 import com.android.dialer.telecom.TelecomUtil;
 import com.android.dialer.util.TouchPointManager;
 import com.android.incallui.InCallOrientationEventListener.ScreenOrientation;
@@ -66,7 +67,6 @@
 import com.android.incallui.latencyreport.LatencyReport;
 import com.android.incallui.legacyblocking.BlockedNumberContentObserver;
 import com.android.incallui.spam.SpamCallListListener;
-import com.android.incallui.util.TelecomCallUtil;
 import com.android.incallui.videosurface.bindings.VideoSurfaceBindings;
 import com.android.incallui.videosurface.protocol.VideoSurfaceTexture;
 import com.android.incallui.videotech.utils.VideoUtils;
@@ -213,7 +213,7 @@
           }
         }
       };
-  
+
   /** Whether or not InCallService is bound to Telecom. */
   private boolean mServiceBound = false;
 
diff --git a/java/com/android/incallui/PhoneLookupHistoryRecorder.java b/java/com/android/incallui/PhoneLookupHistoryRecorder.java
index 2632e65..667c0d1 100644
--- a/java/com/android/incallui/PhoneLookupHistoryRecorder.java
+++ b/java/com/android/incallui/PhoneLookupHistoryRecorder.java
@@ -19,25 +19,18 @@
 import android.content.Context;
 import android.support.annotation.Nullable;
 import android.telecom.Call;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.PhoneNumberUtils;
-import android.telephony.SubscriptionInfo;
-import android.text.TextUtils;
 import com.android.dialer.buildtype.BuildType;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.common.concurrent.DialerExecutors;
-import com.android.dialer.location.CountryDetector;
 import com.android.dialer.phonelookup.PhoneLookupComponent;
 import com.android.dialer.phonelookup.PhoneLookupInfo;
 import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract.PhoneLookupHistory;
-import com.android.dialer.telecom.TelecomUtil;
-import com.android.incallui.util.TelecomCallUtil;
+import com.android.dialer.telecom.TelecomCallUtil;
 import com.google.common.base.Optional;
 import com.google.common.util.concurrent.FutureCallback;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
-import java.util.Locale;
 
 /**
  * Fetches the current {@link PhoneLookupInfo} for the provided call and writes it to the
@@ -61,7 +54,8 @@
           @Override
           public void onSuccess(@Nullable PhoneLookupInfo result) {
             Assert.checkArgument(result != null);
-            Optional<String> normalizedNumber = getNormalizedNumber(appContext, call);
+            Optional<String> normalizedNumber =
+                TelecomCallUtil.getNormalizedNumber(appContext, call);
             if (!normalizedNumber.isPresent()) {
               LogUtil.w("PhoneLookupHistoryRecorder.onSuccess", "couldn't get a number");
               return;
@@ -90,27 +84,4 @@
         },
         DialerExecutors.getLowPriorityThreadPool(appContext));
   }
-
-  private static Optional<String> getNormalizedNumber(Context appContext, Call call) {
-    PhoneAccountHandle phoneAccountHandle = call.getDetails().getAccountHandle();
-    Optional<SubscriptionInfo> subscriptionInfo =
-        TelecomUtil.getSubscriptionInfo(appContext, phoneAccountHandle);
-    String countryCode =
-        subscriptionInfo.isPresent()
-            ? subscriptionInfo.get().getCountryIso()
-            : CountryDetector.getInstance(appContext).getCurrentCountryIso();
-    if (countryCode == null) {
-      LogUtil.w(
-          "PhoneLookupHistoryRecorder.getNormalizedNumber",
-          "couldn't find a country code for call");
-      countryCode = "US";
-    }
-    String rawNumber = TelecomCallUtil.getNumber(call);
-    if (TextUtils.isEmpty(rawNumber)) {
-      return Optional.absent();
-    }
-    String normalizedNumber =
-        PhoneNumberUtils.formatNumberToE164(rawNumber, countryCode.toUpperCase(Locale.US));
-    return normalizedNumber == null ? Optional.of(rawNumber) : Optional.of(normalizedNumber);
-  }
 }
diff --git a/java/com/android/incallui/call/CallList.java b/java/com/android/incallui/call/CallList.java
index d2ac483..150b20e 100644
--- a/java/com/android/incallui/call/CallList.java
+++ b/java/com/android/incallui/call/CallList.java
@@ -40,9 +40,9 @@
 import com.android.dialer.shortcuts.ShortcutUsageReporter;
 import com.android.dialer.spam.Spam;
 import com.android.dialer.spam.SpamComponent;
+import com.android.dialer.telecom.TelecomCallUtil;
 import com.android.incallui.call.DialerCall.State;
 import com.android.incallui.latencyreport.LatencyReport;
-import com.android.incallui.util.TelecomCallUtil;
 import com.android.incallui.videotech.utils.SessionModificationState;
 import java.util.Collection;
 import java.util.Collections;
diff --git a/java/com/android/incallui/call/DialerCall.java b/java/com/android/incallui/call/DialerCall.java
index e8523d6..8120249 100644
--- a/java/com/android/incallui/call/DialerCall.java
+++ b/java/com/android/incallui/call/DialerCall.java
@@ -64,12 +64,12 @@
 import com.android.dialer.logging.ContactLookupResult.Type;
 import com.android.dialer.logging.DialerImpression;
 import com.android.dialer.logging.Logger;
+import com.android.dialer.telecom.TelecomCallUtil;
 import com.android.dialer.telecom.TelecomUtil;
 import com.android.dialer.theme.R;
 import com.android.dialer.util.PermissionsUtil;
 import com.android.incallui.audiomode.AudioModeProvider;
 import com.android.incallui.latencyreport.LatencyReport;
-import com.android.incallui.util.TelecomCallUtil;
 import com.android.incallui.videotech.VideoTech;
 import com.android.incallui.videotech.VideoTech.VideoTechListener;
 import com.android.incallui.videotech.duo.DuoVideoTech;
diff --git a/java/com/android/incallui/util/TelecomCallUtil.java b/java/com/android/incallui/util/TelecomCallUtil.java
deleted file mode 100644
index 8855543..0000000
--- a/java/com/android/incallui/util/TelecomCallUtil.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2015 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.incallui.util;
-
-import android.net.Uri;
-import android.telecom.Call;
-import android.telephony.PhoneNumberUtils;
-
-/**
- * Class to provide a standard interface for obtaining information from the underlying
- * android.telecom.Call. Much of this should be obtained through the incall.Call, but on occasion we
- * need to interact with the telecom.Call directly (eg. call blocking, before the incall.Call has
- * been created).
- */
-public class TelecomCallUtil {
-
-  // Whether the call handle is an emergency number.
-  public static boolean isEmergencyCall(Call call) {
-    Uri handle = call.getDetails().getHandle();
-    return PhoneNumberUtils.isEmergencyNumber(handle == null ? "" : handle.getSchemeSpecificPart());
-  }
-
-  public static String getNumber(Call call) {
-    if (call == null) {
-      return null;
-    }
-    if (call.getDetails().getGatewayInfo() != null) {
-      return call.getDetails().getGatewayInfo().getOriginalAddress().getSchemeSpecificPart();
-    }
-    Uri handle = getHandle(call);
-    return handle == null ? null : handle.getSchemeSpecificPart();
-  }
-
-  public static Uri getHandle(Call call) {
-    return call == null ? null : call.getDetails().getHandle();
-  }
-}
diff --git a/java/com/android/newbubble/NewBubble.java b/java/com/android/newbubble/NewBubble.java
index 23c4411..34a9585 100644
--- a/java/com/android/newbubble/NewBubble.java
+++ b/java/com/android/newbubble/NewBubble.java
@@ -47,11 +47,14 @@
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
+import android.view.View.AccessibilityDelegate;
 import android.view.ViewGroup;
 import android.view.ViewPropertyAnimator;
 import android.view.ViewTreeObserver.OnPreDrawListener;
 import android.view.WindowManager;
 import android.view.WindowManager.LayoutParams;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
 import android.view.animation.AnticipateInterpolator;
 import android.view.animation.OvershootInterpolator;
 import android.widget.ImageView;
@@ -228,6 +231,8 @@
     if (isUserAction) {
       logBasicOrCallImpression(DialerImpression.Type.BUBBLE_PRIMARY_BUTTON_EXPAND);
     }
+    setPrimaryButtonAccessibilityAction(
+        context.getString(R.string.a11y_bubble_primary_button_collapse_action));
     viewHolder.setDrawerVisibility(View.INVISIBLE);
     View expandedView = viewHolder.getExpandedView();
     expandedView
@@ -310,6 +315,8 @@
     if (isUserAction && collapseEndAction == CollapseEnd.NOTHING) {
       logBasicOrCallImpression(DialerImpression.Type.BUBBLE_COLLAPSE_BY_USER);
     }
+    setPrimaryButtonAccessibilityAction(
+        context.getString(R.string.a11y_bubble_primary_button_expand_action));
     // Animate expanded view to move from its position to above primary button and hide
     collapseAnimation =
         expandedView
@@ -448,6 +455,9 @@
     viewHolder.setChildClickable(true);
     visibility = Visibility.ENTERING;
 
+    setPrimaryButtonAccessibilityAction(
+        context.getString(R.string.a11y_bubble_primary_button_expand_action));
+
     // Show bubble animation: scale the whole bubble to 1, and change avatar+icon's alpha to 1
     ObjectAnimator scaleXAnimator =
         ObjectAnimator.ofFloat(viewHolder.getPrimaryButton(), "scaleX", 1);
@@ -726,6 +736,11 @@
     exitAnimatorSet.addListener(
         new AnimatorListenerAdapter() {
           @Override
+          public void onAnimationStart(Animator animation) {
+            viewHolder.getPrimaryButton().setAccessibilityDelegate(null);
+          }
+
+          @Override
           public void onAnimationEnd(Animator animation) {
             afterHiding.run();
           }
@@ -793,6 +808,7 @@
     button.setChecked(action.isChecked());
     button.setEnabled(action.isEnabled());
     button.setText(action.getName());
+    button.setContentDescription(action.getName());
     button.setOnClickListener(v -> doAction(action));
   }
 
@@ -822,6 +838,8 @@
     viewHolder
         .getPrimaryIcon()
         .setTranslationX(isDrawingFromRight() ? -primaryIconMoveDistance : 0);
+    setPrimaryButtonAccessibilityAction(
+        context.getString(R.string.a11y_bubble_primary_button_expand_action));
 
     update();
 
@@ -883,6 +901,22 @@
     }
   }
 
+  private void setPrimaryButtonAccessibilityAction(String description) {
+    viewHolder
+        .getPrimaryButton()
+        .setAccessibilityDelegate(
+            new AccessibilityDelegate() {
+              @Override
+              public void onInitializeAccessibilityNodeInfo(View v, AccessibilityNodeInfo info) {
+                super.onInitializeAccessibilityNodeInfo(v, info);
+
+                AccessibilityAction clickAction =
+                    new AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, description);
+                info.addAction(clickAction);
+              }
+            });
+  }
+
   @VisibleForTesting
   class ViewHolder {
 
diff --git a/java/com/android/newbubble/res/layout/new_bubble_base.xml b/java/com/android/newbubble/res/layout/new_bubble_base.xml
index 8d47716..216dce0 100644
--- a/java/com/android/newbubble/res/layout/new_bubble_base.xml
+++ b/java/com/android/newbubble/res/layout/new_bubble_base.xml
@@ -38,6 +38,7 @@
         android:layout_marginEnd="@dimen/bubble_shadow_padding_size_horizontal"
         android:layout_marginTop="@dimen/bubble_shadow_padding_size_vertical"
         android:layout_marginBottom="@dimen/bubble_shadow_padding_size_vertical"
+        android:contentDescription="@string/a11y_bubble_description"
         android:background="@drawable/bubble_shape_circle"
         android:measureAllChildren="false"
         android:elevation="@dimen/bubble_elevation"
diff --git a/java/com/android/newbubble/res/values/strings.xml b/java/com/android/newbubble/res/values/strings.xml
new file mode 100644
index 0000000..5b82b18
--- /dev/null
+++ b/java/com/android/newbubble/res/values/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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
+  -->
+
+<resources>
+  <!-- A string for Talkback to read when accessibility user touch bubble. -->
+  <string name="a11y_bubble_description">Dialer bubble</string>
+  <!-- A string to describe available action for accessibility user. It will be read as "Actions:
+    double tap to expand call action menu". -->
+  <string name="a11y_bubble_primary_button_expand_action">Expand call action menu</string>
+  <!-- A string to describe available action for accessibility user. It will be read as "Actions:
+    double tap to collapse call action menu". -->
+  <string name="a11y_bubble_primary_button_collapse_action">Collapse call action menu</string>
+</resources>
\ No newline at end of file
diff --git a/java/com/android/voicemail/impl/VoicemailClientImpl.java b/java/com/android/voicemail/impl/VoicemailClientImpl.java
index 60fc806..75d6dfc 100644
--- a/java/com/android/voicemail/impl/VoicemailClientImpl.java
+++ b/java/com/android/voicemail/impl/VoicemailClientImpl.java
@@ -130,7 +130,7 @@
     }
 
     TranscriptionConfigProvider provider = new TranscriptionConfigProvider(context);
-    if (!provider.isVoicemailTranscriptionEnabled()) {
+    if (!provider.isVoicemailTranscriptionAvailable()) {
       LogUtil.i(
           "VoicemailClientImpl.isVoicemailTranscriptionAvailable", "feature disabled by config");
       return false;
diff --git a/java/com/android/voicemail/impl/transcribe/TranscriptionConfigProvider.java b/java/com/android/voicemail/impl/transcribe/TranscriptionConfigProvider.java
index 3d1755b..54a1ae4 100644
--- a/java/com/android/voicemail/impl/transcribe/TranscriptionConfigProvider.java
+++ b/java/com/android/voicemail/impl/transcribe/TranscriptionConfigProvider.java
@@ -28,9 +28,10 @@
     this.context = context;
   }
 
-  public boolean isVoicemailTranscriptionEnabled() {
+  public boolean isVoicemailTranscriptionAvailable() {
     return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
-        && ConfigProviderBindings.get(context).getBoolean("voicemail_transcription_enabled", false);
+        && ConfigProviderBindings.get(context)
+            .getBoolean("voicemail_transcription_available", false);
   }
 
   public String getServerAddress() {
diff --git a/java/com/android/voicemail/impl/transcribe/TranscriptionService.java b/java/com/android/voicemail/impl/transcribe/TranscriptionService.java
index a19ab62..0f53003 100644
--- a/java/com/android/voicemail/impl/transcribe/TranscriptionService.java
+++ b/java/com/android/voicemail/impl/transcribe/TranscriptionService.java
@@ -142,8 +142,8 @@
   public boolean onStartJob(JobParameters params) {
     Assert.isMainThread();
     LogUtil.enterBlock("TranscriptionService.onStartJob");
-    if (!getConfigProvider().isVoicemailTranscriptionEnabled()) {
-      LogUtil.i("TranscriptionService.onStartJob", "transcription not enabled, exiting.");
+    if (!getConfigProvider().isVoicemailTranscriptionAvailable()) {
+      LogUtil.i("TranscriptionService.onStartJob", "transcription not available, exiting.");
       return false;
     } else if (TextUtils.isEmpty(getConfigProvider().getServerAddress())) {
       LogUtil.i("TranscriptionService.onStartJob", "transcription server not configured, exiting.");