Merge changes Ibab0bc7e,I1cb26187,I64225b78,I4806ab65,I7d431e74, ... am: 84b862acd2
am: e2c2c5b740

Change-Id: Ie7aa64c20c7f8a0d792812fb0c062f870bd2abe3
diff --git a/java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java b/java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java
index bb0d3c6..ca8ed29 100644
--- a/java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java
+++ b/java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java
@@ -23,11 +23,6 @@
 /** Compatibility class for {@link android.telecom.TelecomManager}. */
 public class TelecomManagerCompat {
 
-  // TODO(mdooley): remove once this is available in android.telecom.Call
-  // a bug
-  public static final String EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS =
-      "android.telecom.extra.LAST_EMERGENCY_CALLBACK_TIME_MILLIS";
-
   // Constants from http://cs/android/frameworks/base/telecomm/java/android/telecom/Call.java.
   public static final String EVENT_REQUEST_HANDOVER = "android.telecom.event.REQUEST_HANDOVER";
   public static final String EXTRA_HANDOVER_PHONE_ACCOUNT_HANDLE =
diff --git a/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java b/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java
index 5fed683..cad2eb7 100644
--- a/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java
+++ b/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java
@@ -41,7 +41,6 @@
 import com.android.dialer.speeddial.loader.UiItemLoaderComponent;
 import com.android.dialer.storage.StorageComponent;
 import com.android.dialer.strictmode.StrictModeComponent;
-import com.android.incallui.audiomode.BluetoothDeviceProviderComponent;
 import com.android.incallui.calllocation.CallLocationComponent;
 import com.android.incallui.maps.MapsComponent;
 import com.android.incallui.speakeasy.SpeakEasyComponent;
@@ -53,7 +52,6 @@
  */
 public interface BaseDialerRootComponent
     extends ActiveCallsComponent.HasComponent,
-        BluetoothDeviceProviderComponent.HasComponent,
         BubbleComponent.HasComponent,
         CallLocationComponent.HasComponent,
         CallLogComponent.HasComponent,
diff --git a/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java b/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java
index cb84a28..cd1752d 100644
--- a/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java
+++ b/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java
@@ -192,8 +192,6 @@
     nameView.setText(headerInfo.getPrimaryText());
     numberView.setText(headerInfo.getSecondaryText());
 
-    // TODO(a bug): show SIM info in the TextView returned by getNetworkView().
-
     setCallbackAction(callbackAction);
   }
 
diff --git a/java/com/android/dialer/calldetails/proto/call_details_header_info.proto b/java/com/android/dialer/calldetails/proto/call_details_header_info.proto
index ea7ba1e..e2532d5 100644
--- a/java/com/android/dialer/calldetails/proto/call_details_header_info.proto
+++ b/java/com/android/dialer/calldetails/proto/call_details_header_info.proto
@@ -31,6 +31,4 @@
   //   "Blocked • Mobile • 555-1234", and
   //   "Spam • Mobile • 555-1234".
   optional string secondary_text = 4;
-
-  // TODO(a bug): Add SIM info.
 }
\ No newline at end of file
diff --git a/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java b/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java
index 44a08c7..c02d80e 100644
--- a/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java
+++ b/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java
@@ -268,6 +268,7 @@
   private void setOnClickListenerForRow(CoalescedRow row) {
     if (!PhoneNumberHelper.canPlaceCallsTo(
         row.getNumber().getNormalizedNumber(), row.getNumberPresentation())) {
+      itemView.setOnClickListener(null);
       return;
     }
     itemView.setOnClickListener(view -> CallLogRowActions.startCallForRow(activity, row));
diff --git a/java/com/android/dialer/calllog/ui/menu/BottomSheetHeader.java b/java/com/android/dialer/calllog/ui/menu/BottomSheetHeader.java
index d87888d..4e25ced 100644
--- a/java/com/android/dialer/calllog/ui/menu/BottomSheetHeader.java
+++ b/java/com/android/dialer/calllog/ui/menu/BottomSheetHeader.java
@@ -34,6 +34,7 @@
             NumberAttributesConverter.toPhotoInfoBuilder(row.getNumberAttributes())
                 .setFormattedNumber(row.getFormattedNumber())
                 .setIsVideo((row.getFeatures() & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO)
+                .setIsVoicemail(row.getIsVoicemailCall())
                 .setIsRtt(
                     BuildCompat.isAtLeastP()
                         && (row.getFeatures() & Calls.FEATURES_RTT) == Calls.FEATURES_RTT)
diff --git a/java/com/android/dialer/calllog/ui/menu/Modules.java b/java/com/android/dialer/calllog/ui/menu/Modules.java
index a56d6d5..b06e0fb 100644
--- a/java/com/android/dialer/calllog/ui/menu/Modules.java
+++ b/java/com/android/dialer/calllog/ui/menu/Modules.java
@@ -20,152 +20,53 @@
 import android.provider.CallLog.Calls;
 import android.support.v4.os.BuildCompat;
 import android.text.TextUtils;
-import com.android.dialer.blockreportspam.BlockReportSpamDialogInfo;
 import com.android.dialer.calldetails.CallDetailsActivity;
 import com.android.dialer.calldetails.CallDetailsHeaderInfo;
-import com.android.dialer.callintent.CallInitiationType;
-import com.android.dialer.callintent.CallIntentBuilder;
 import com.android.dialer.calllog.model.CoalescedRow;
 import com.android.dialer.calllogutils.CallLogEntryText;
 import com.android.dialer.calllogutils.NumberAttributesConverter;
-import com.android.dialer.duo.Duo;
-import com.android.dialer.duo.DuoComponent;
 import com.android.dialer.glidephotomanager.PhotoInfo;
-import com.android.dialer.historyitemactions.DividerModule;
-import com.android.dialer.historyitemactions.DuoCallModule;
 import com.android.dialer.historyitemactions.HistoryItemActionModule;
+import com.android.dialer.historyitemactions.HistoryItemActionModuleInfo;
+import com.android.dialer.historyitemactions.HistoryItemActionModulesBuilder;
 import com.android.dialer.historyitemactions.IntentModule;
-import com.android.dialer.historyitemactions.SharedModules;
-import com.android.dialer.logging.ReportingLocation;
 import com.android.dialer.phonenumberutil.PhoneNumberHelper;
-import com.android.dialer.util.CallUtil;
-import com.google.common.base.Optional;
-import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 
 /**
- * Configures the modules for the bottom sheet; these are the rows below the top row (primary
- * action) in the bottom sheet.
+ * Configures the modules for the bottom sheet; these are the rows below the top row (contact info)
+ * in the bottom sheet.
  */
-@SuppressWarnings("Guava")
 final class Modules {
 
+  /**
+   * Returns a list of {@link HistoryItemActionModule HistoryItemActionModules}, which are items in
+   * the bottom sheet.
+   */
   static List<HistoryItemActionModule> fromRow(Context context, CoalescedRow row) {
-    // Conditionally add each module, which are items in the bottom sheet's menu.
-    List<HistoryItemActionModule> modules = new ArrayList<>();
-
-    String normalizedNumber = row.getNumber().getNormalizedNumber();
-    boolean canPlaceCalls =
-        PhoneNumberHelper.canPlaceCallsTo(normalizedNumber, row.getNumberPresentation());
-
-    if (canPlaceCalls) {
-      modules.addAll(createModulesForCalls(context, row, normalizedNumber));
-      Optional<HistoryItemActionModule> moduleForSendingTextMessage =
-          SharedModules.createModuleForSendingTextMessage(
-              context, normalizedNumber, row.getNumberAttributes().getIsBlocked());
-      if (moduleForSendingTextMessage.isPresent()) {
-        modules.add(moduleForSendingTextMessage.get());
-      }
-    }
-
-    if (!modules.isEmpty()) {
-      modules.add(new DividerModule());
-    }
+    HistoryItemActionModulesBuilder modulesBuilder =
+        new HistoryItemActionModulesBuilder(context, buildModuleInfo(row));
 
 
     // TODO(zachh): Module for CallComposer.
 
-    if (canPlaceCalls) {
-      Optional<HistoryItemActionModule> moduleForAddingToContacts =
-          SharedModules.createModuleForAddingToContacts(
-              context,
-              row.getNumber(),
-              row.getNumberAttributes().getName(),
-              row.getNumberAttributes().getLookupUri(),
-              row.getNumberAttributes().getIsBlocked(),
-              row.getNumberAttributes().getIsSpam());
-      if (moduleForAddingToContacts.isPresent()) {
-        modules.add(moduleForAddingToContacts.get());
-      }
-
-      BlockReportSpamDialogInfo blockReportSpamDialogInfo =
-          BlockReportSpamDialogInfo.newBuilder()
-              .setNormalizedNumber(row.getNumber().getNormalizedNumber())
-              .setCountryIso(row.getNumber().getCountryIso())
-              .setCallType(row.getCallType())
-              .setReportingLocation(ReportingLocation.Type.CALL_LOG_HISTORY)
-              .setContactSource(row.getNumberAttributes().getContactSource())
-              .build();
-      modules.addAll(
-          SharedModules.createModulesHandlingBlockedOrSpamNumber(
-              context,
-              blockReportSpamDialogInfo,
-              row.getNumberAttributes().getIsBlocked(),
-              row.getNumberAttributes().getIsSpam()));
-
-      Optional<HistoryItemActionModule> moduleForCopyingNumber =
-          SharedModules.createModuleForCopyingNumber(context, normalizedNumber);
-      if (moduleForCopyingNumber.isPresent()) {
-        modules.add(moduleForCopyingNumber.get());
-      }
+    if (PhoneNumberHelper.canPlaceCallsTo(
+        row.getNumber().getNormalizedNumber(), row.getNumberPresentation())) {
+      modulesBuilder
+          .addModuleForVoiceCall()
+          .addModuleForVideoCall()
+          .addModuleForSendingTextMessage()
+          .addModuleForDivider()
+          .addModuleForAddingToContacts()
+          .addModuleForBlockedOrSpamNumber()
+          .addModuleForCopyingNumber();
     }
 
+    List<HistoryItemActionModule> modules = modulesBuilder.build();
+
+    // Add modules only available in the call log.
     modules.add(createModuleForAccessingCallDetails(context, row));
-
     modules.add(new DeleteCallLogItemModule(context, row.getCoalescedIds()));
-
-    return modules;
-  }
-
-  private static List<HistoryItemActionModule> createModulesForCalls(
-      Context context, CoalescedRow row, String normalizedNumber) {
-    // Don't add call options if a number is blocked.
-    if (row.getNumberAttributes().getIsBlocked()) {
-      return Collections.emptyList();
-    }
-
-    boolean isDuoCall =
-        DuoComponent.get(context).getDuo().isDuoAccount(row.getPhoneAccountComponentName());
-
-    List<HistoryItemActionModule> modules = new ArrayList<>();
-
-    // Add an audio call item
-    // TODO(zachh): Support post-dial digits; consider using DialerPhoneNumber.
-    CallIntentBuilder callIntentBuilder =
-        new CallIntentBuilder(normalizedNumber, CallInitiationType.Type.CALL_LOG)
-            .setAllowAssistedDial(canSupportAssistedDialing(row));
-    // Leave PhoneAccountHandle blank so regular PreCall logic will be used. The account the call
-    // was made/received in should be ignored for audio and carrier video calls.
-    // TODO(a bug): figure out the correct video call behavior
-    modules.add(IntentModule.newCallModule(context, callIntentBuilder));
-
-    // If the call log entry is for a spam call, nothing more to be done.
-    if (row.getNumberAttributes().getIsSpam()) {
-      return modules;
-    }
-
-    // If the call log entry is for a video call, add the corresponding video call options.
-    // Note that if the entry is for a Duo video call but Duo is not available, we will fall back to
-    // a carrier video call.
-    if ((row.getFeatures() & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO) {
-      modules.add(
-          isDuoCall && canPlaceDuoCall(context, normalizedNumber)
-              ? new DuoCallModule(context, normalizedNumber)
-              : IntentModule.newCallModule(context, callIntentBuilder.setIsVideoCall(true)));
-      return modules;
-    }
-
-    // At this point, the call log entry is for an audio call. We will also show a video call option
-    // if the video capability is present.
-    //
-    // The carrier video call option takes precedence over Duo.
-    if (canPlaceCarrierVideoCall(context, row)) {
-      modules.add(IntentModule.newCallModule(context, callIntentBuilder.setIsVideoCall(true)));
-    } else if (canPlaceDuoCall(context, normalizedNumber)) {
-      modules.add(new DuoCallModule(context, normalizedNumber));
-    }
-
     return modules;
   }
 
@@ -208,30 +109,27 @@
         .build();
   }
 
-  private static boolean canPlaceDuoCall(Context context, String phoneNumber) {
-    Duo duo = DuoComponent.get(context).getDuo();
-
-    return duo.isInstalled(context)
-        && duo.isEnabled(context)
-        && duo.isActivated(context)
-        && duo.isReachable(context, phoneNumber);
-  }
-
-  private static boolean canPlaceCarrierVideoCall(Context context, CoalescedRow row) {
-    int carrierVideoAvailability = CallUtil.getVideoCallingAvailability(context);
-    boolean isCarrierVideoCallingEnabled =
-        ((carrierVideoAvailability & CallUtil.VIDEO_CALLING_ENABLED)
-            == CallUtil.VIDEO_CALLING_ENABLED);
-    boolean canRelyOnCarrierVideoPresence =
-        ((carrierVideoAvailability & CallUtil.VIDEO_CALLING_PRESENCE)
-            == CallUtil.VIDEO_CALLING_PRESENCE);
-
-    return isCarrierVideoCallingEnabled
-        && canRelyOnCarrierVideoPresence
-        && row.getNumberAttributes().getCanSupportCarrierVideoCall();
-  }
-
   private static boolean canSupportAssistedDialing(CoalescedRow row) {
     return !TextUtils.isEmpty(row.getNumberAttributes().getLookupUri());
   }
+
+  private static HistoryItemActionModuleInfo buildModuleInfo(CoalescedRow row) {
+    return HistoryItemActionModuleInfo.newBuilder()
+        .setNormalizedNumber(row.getNumber().getNormalizedNumber())
+        .setCountryIso(row.getNumber().getCountryIso())
+        .setName(row.getNumberAttributes().getName())
+        .setCallType(row.getCallType())
+        .setFeatures(row.getFeatures())
+        .setLookupUri(row.getNumberAttributes().getLookupUri())
+        .setPhoneAccountComponentName(row.getPhoneAccountComponentName())
+        .setCanReportAsInvalidNumber(row.getNumberAttributes().getCanReportAsInvalidNumber())
+        .setCanSupportAssistedDialing(canSupportAssistedDialing(row))
+        .setCanSupportCarrierVideoCall(row.getNumberAttributes().getCanSupportCarrierVideoCall())
+        .setIsBlocked(row.getNumberAttributes().getIsBlocked())
+        .setIsSpam(row.getNumberAttributes().getIsSpam())
+        .setIsVoicemailCall(row.getIsVoicemailCall())
+        .setContactSource(row.getNumberAttributes().getContactSource())
+        .setHost(HistoryItemActionModuleInfo.Host.CALL_LOG)
+        .build();
+  }
 }
diff --git a/java/com/android/dialer/configprovider/SharedPrefConfigProvider.java b/java/com/android/dialer/configprovider/SharedPrefConfigProvider.java
index ce95c57..54e9c9a 100644
--- a/java/com/android/dialer/configprovider/SharedPrefConfigProvider.java
+++ b/java/com/android/dialer/configprovider/SharedPrefConfigProvider.java
@@ -98,6 +98,10 @@
     sharedPreferences.edit().putBoolean(PREF_PREFIX + key, value).apply();
   }
 
+  public void putLong(String key, long value) {
+    sharedPreferences.edit().putLong(PREF_PREFIX + key, value).apply();
+  }
+
   @Override
   public String getString(String key, String defaultValue) {
     // Reading shared prefs on the main thread is generally safe since a single instance is cached.
diff --git a/java/com/android/dialer/historyitemactions/BlockReportSpamModules.java b/java/com/android/dialer/historyitemactions/BlockReportSpamModules.java
new file mode 100644
index 0000000..396c033
--- /dev/null
+++ b/java/com/android/dialer/historyitemactions/BlockReportSpamModules.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2018 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.historyitemactions;
+
+import android.content.Context;
+import com.android.dialer.blockreportspam.BlockReportSpamDialogInfo;
+import com.android.dialer.blockreportspam.ShowBlockReportSpamDialogNotifier;
+
+/** Modules for blocking/unblocking a number and/or reporting it as spam/not spam. */
+final class BlockReportSpamModules {
+
+  private BlockReportSpamModules() {}
+
+  static HistoryItemActionModule moduleForMarkingNumberAsNotSpam(
+      Context context, BlockReportSpamDialogInfo blockReportSpamDialogInfo) {
+
+    return new HistoryItemActionModule() {
+      @Override
+      public int getStringId() {
+        return R.string.not_spam;
+      }
+
+      @Override
+      public int getDrawableId() {
+        return R.drawable.quantum_ic_report_off_vd_theme_24;
+      }
+
+      @Override
+      public boolean onClick() {
+        ShowBlockReportSpamDialogNotifier.notifyShowDialogToReportNotSpam(
+            context, blockReportSpamDialogInfo);
+        return true; // Close the bottom sheet.
+      }
+    };
+  }
+
+  static HistoryItemActionModule moduleForBlockingNumber(
+      Context context, BlockReportSpamDialogInfo blockReportSpamDialogInfo) {
+
+    return new HistoryItemActionModule() {
+      @Override
+      public int getStringId() {
+        return R.string.block_number;
+      }
+
+      @Override
+      public int getDrawableId() {
+        return R.drawable.quantum_ic_block_vd_theme_24;
+      }
+
+      @Override
+      public boolean onClick() {
+        ShowBlockReportSpamDialogNotifier.notifyShowDialogToBlockNumber(
+            context, blockReportSpamDialogInfo);
+        return true; // Close the bottom sheet.
+      }
+    };
+  }
+
+  static HistoryItemActionModule moduleForUnblockingNumber(
+      Context context, BlockReportSpamDialogInfo blockReportSpamDialogInfo) {
+
+    return new HistoryItemActionModule() {
+      @Override
+      public int getStringId() {
+        return R.string.unblock_number;
+      }
+
+      @Override
+      public int getDrawableId() {
+        return R.drawable.quantum_ic_unblock_vd_theme_24;
+      }
+
+      @Override
+      public boolean onClick() {
+        ShowBlockReportSpamDialogNotifier.notifyShowDialogToUnblockNumber(
+            context, blockReportSpamDialogInfo);
+
+        return true; // Close the bottom sheet.
+      }
+    };
+  }
+
+  static HistoryItemActionModule moduleForBlockingNumberAndOptionallyReportingSpam(
+      Context context, BlockReportSpamDialogInfo blockReportSpamDialogInfo) {
+
+    return new HistoryItemActionModule() {
+      @Override
+      public int getStringId() {
+        return R.string.block_and_optionally_report_spam;
+      }
+
+      @Override
+      public int getDrawableId() {
+        return R.drawable.quantum_ic_block_vd_theme_24;
+      }
+
+      @Override
+      public boolean onClick() {
+        ShowBlockReportSpamDialogNotifier.notifyShowDialogToBlockNumberAndOptionallyReportSpam(
+            context, blockReportSpamDialogInfo);
+        return true; // Close the bottom sheet.
+      }
+    };
+  }
+}
diff --git a/java/com/android/dialer/historyitemactions/HistoryItemActionModulesBuilder.java b/java/com/android/dialer/historyitemactions/HistoryItemActionModulesBuilder.java
new file mode 100644
index 0000000..9af08be
--- /dev/null
+++ b/java/com/android/dialer/historyitemactions/HistoryItemActionModulesBuilder.java
@@ -0,0 +1,422 @@
+/*
+ * Copyright (C) 2018 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.historyitemactions;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract;
+import android.text.TextUtils;
+import com.android.dialer.blockreportspam.BlockReportSpamDialogInfo;
+import com.android.dialer.callintent.CallInitiationType;
+import com.android.dialer.callintent.CallIntentBuilder;
+import com.android.dialer.clipboard.ClipboardUtils;
+import com.android.dialer.common.Assert;
+import com.android.dialer.duo.Duo;
+import com.android.dialer.duo.DuoComponent;
+import com.android.dialer.logging.ReportingLocation;
+import com.android.dialer.util.CallUtil;
+import com.android.dialer.util.UriUtils;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Builds a list of {@link HistoryItemActionModule HistoryItemActionModules}.
+ *
+ * <p>Example usage:
+ *
+ * <pre><code>
+ *    // Create a HistoryItemActionModuleInfo proto with the information you have.
+ *    // You can simply skip a field if there is no information for it.
+ *    HistoryItemActionModuleInfo moduleInfo =
+ *        HistoryItemActionModuleInfo.newBuilder()
+ *            .setNormalizedNumber("+16502530000")
+ *            .setCountryIso("US")
+ *            .setName("Google")
+ *            .build();
+ *
+ *    // Initialize the builder using the module info above.
+ *    // Note that some modules require an activity context to work so it is preferred to pass one
+ *    // instead of an application context to the builder.
+ *    HistoryItemActionModulesBuilder modulesBuilder =
+ *        new HistoryItemActionModulesBuilder(activityContext, moduleInfo);
+ *
+ *    // Add all modules you want in the order you like.
+ *    // If a module shouldn't be added according to the module info, it won't be.
+ *    // For example, if the module info is not for a video call and doesn't indicate the presence
+ *    // of video calling capabilities, calling addModuleForVideoCall() is a no-op.
+ *    modulesBuilder
+ *        .addModuleForVoiceCall()
+ *        .addModuleForVideoCall()
+ *        .addModuleForSendingTextMessage()
+ *        .addModuleForDivider()
+ *        .addModuleForAddingToContacts()
+ *        .addModuleForBlockedOrSpamNumber()
+ *        .addModuleForCopyingNumber();
+ *
+ *    List<HistoryItemActionModule> modules = modulesBuilder.build();
+ * </code></pre>
+ */
+public final class HistoryItemActionModulesBuilder {
+
+  private final Context context;
+  private final HistoryItemActionModuleInfo moduleInfo;
+  private final List<HistoryItemActionModule> modules;
+
+  public HistoryItemActionModulesBuilder(Context context, HistoryItemActionModuleInfo moduleInfo) {
+    Assert.checkArgument(
+        moduleInfo.getHost() != HistoryItemActionModuleInfo.Host.UNKNOWN,
+        "A host must be specified.");
+
+    this.context = context;
+    this.moduleInfo = moduleInfo;
+    this.modules = new ArrayList<>();
+  }
+
+  public List<HistoryItemActionModule> build() {
+    return new ArrayList<>(modules);
+  }
+
+  /**
+   * Adds a module for placing a voice call.
+   *
+   * <p>The method is a no-op if the number is blocked.
+   */
+  public HistoryItemActionModulesBuilder addModuleForVoiceCall() {
+    if (moduleInfo.getIsBlocked()) {
+      return this;
+    }
+
+    // TODO(zachh): Support post-dial digits; consider using DialerPhoneNumber.
+    // Do not set PhoneAccountHandle so that regular PreCall logic will be used. The account used to
+    // place or receive the call should be ignored for voice calls.
+    CallIntentBuilder callIntentBuilder =
+        new CallIntentBuilder(moduleInfo.getNormalizedNumber(), getCallInitiationType())
+            .setAllowAssistedDial(moduleInfo.getCanSupportAssistedDialing());
+    modules.add(IntentModule.newCallModule(context, callIntentBuilder));
+    return this;
+  }
+
+  /**
+   * Adds a module for a carrier video call *or* a Duo video call.
+   *
+   * <p>This method is a no-op if
+   *
+   * <ul>
+   *   <li>the call is one made to a voicemail box,
+   *   <li>the number is blocked, or
+   *   <li>the number is marked as spam.
+   * </ul>
+   *
+   * <p>If the provided module info is for a Duo video call and Duo is available, add a Duo video
+   * call module.
+   *
+   * <p>If the provided module info is for a Duo video call but Duo is unavailable, add a carrier
+   * video call module.
+   *
+   * <p>If the provided module info is for a carrier video call, add a carrier video call module.
+   *
+   * <p>If the provided module info is for a voice call and the device has carrier video call
+   * capability, add a carrier video call module.
+   *
+   * <p>If the provided module info is for a voice call, the device doesn't have carrier video call
+   * capability, and Duo is available, add a Duo video call module.
+   */
+  public HistoryItemActionModulesBuilder addModuleForVideoCall() {
+    if (moduleInfo.getIsVoicemailCall() || moduleInfo.getIsBlocked() || moduleInfo.getIsSpam()) {
+      return this;
+    }
+
+    // Do not set PhoneAccountHandle so that regular PreCall logic will be used. The account used to
+    // place or receive the call should be ignored for carrier video calls.
+    // TODO(a bug): figure out the correct video call behavior
+    HistoryItemActionModule carrierVideoCallModule =
+        IntentModule.newCallModule(
+            context,
+            new CallIntentBuilder(moduleInfo.getNormalizedNumber(), getCallInitiationType())
+                .setAllowAssistedDial(moduleInfo.getCanSupportAssistedDialing())
+                .setIsVideoCall(true));
+    HistoryItemActionModule duoVideoCallModule =
+        new DuoCallModule(context, moduleInfo.getNormalizedNumber());
+
+    // If the module info is for a video call, add an appropriate video call module.
+    if ((moduleInfo.getFeatures() & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO) {
+      modules.add(isDuoCall() && canPlaceDuoCall() ? duoVideoCallModule : carrierVideoCallModule);
+      return this;
+    }
+
+    // At this point, the module info is for an audio call. We will also add a video call module if
+    // the video capability is present.
+    //
+    // The carrier video call module takes precedence over the Duo module.
+    if (canPlaceCarrierVideoCall()) {
+      modules.add(carrierVideoCallModule);
+    } else if (canPlaceDuoCall()) {
+      modules.add(duoVideoCallModule);
+    }
+    return this;
+  }
+
+  /**
+   * Adds a module for sending text messages.
+   *
+   * <p>The method is a no-op if
+   *
+   * <ul>
+   *   <li>the call is one made to a voicemail box,
+   *   <li>the number is blocked, or
+   *   <li>the number is empty.
+   * </ul>
+   */
+  public HistoryItemActionModulesBuilder addModuleForSendingTextMessage() {
+    // TODO(zachh): There are other conditions where this module should not be shown
+    // (e.g., business numbers).
+    if (moduleInfo.getIsVoicemailCall()
+        || moduleInfo.getIsBlocked()
+        || TextUtils.isEmpty(moduleInfo.getNormalizedNumber())) {
+      return this;
+    }
+
+    modules.add(
+        IntentModule.newModuleForSendingTextMessage(context, moduleInfo.getNormalizedNumber()));
+    return this;
+  }
+
+  /**
+   * Adds a module for a divider.
+   *
+   * <p>The method is a no-op if the divider module will be the first module.
+   */
+  public HistoryItemActionModulesBuilder addModuleForDivider() {
+    if (modules.isEmpty()) {
+      return this;
+    }
+
+    modules.add(new DividerModule());
+    return this;
+  }
+
+  /**
+   * Adds a module for adding a number to Contacts.
+   *
+   * <p>The method is a no-op if
+   *
+   * <ul>
+   *   <li>the call is one made to a voicemail box,
+   *   <li>the number is blocked,
+   *   <li>the number is marked as spam,
+   *   <li>the number is empty, or
+   *   <li>the number belongs to an existing contact.
+   * </ul>
+   */
+  public HistoryItemActionModulesBuilder addModuleForAddingToContacts() {
+    if (moduleInfo.getIsVoicemailCall()
+        || moduleInfo.getIsBlocked()
+        || moduleInfo.getIsSpam()
+        || isExistingContact()
+        || TextUtils.isEmpty(moduleInfo.getNormalizedNumber())) {
+      return this;
+    }
+
+    Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
+    intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
+    intent.putExtra(ContactsContract.Intents.Insert.PHONE, moduleInfo.getNormalizedNumber());
+
+    if (!TextUtils.isEmpty(moduleInfo.getName())) {
+      intent.putExtra(ContactsContract.Intents.Insert.NAME, moduleInfo.getName());
+    }
+
+    modules.add(
+        new IntentModule(
+            context,
+            intent,
+            R.string.add_to_contacts,
+            R.drawable.quantum_ic_person_add_vd_theme_24));
+    return this;
+  }
+
+  /**
+   * Add modules for blocking/unblocking a number and/or marking it as spam/not spam.
+   *
+   * <p>The method is a no-op if the call is one made to a voicemail box.
+   *
+   * <p>If a number is marked as spam, add two modules:
+   *
+   * <ul>
+   *   <li>"Not spam" and "Block", or
+   *   <li>"Not spam" and "Unblock".
+   * </ul>
+   *
+   * <p>If a number is blocked but not marked as spam, add the "Unblock" module.
+   *
+   * <p>If a number is not blocked or marked as spam, add the "Block/Report spam" module.
+   */
+  public HistoryItemActionModulesBuilder addModuleForBlockedOrSpamNumber() {
+    if (moduleInfo.getIsVoicemailCall()) {
+      return this;
+    }
+
+    BlockReportSpamDialogInfo blockReportSpamDialogInfo =
+        BlockReportSpamDialogInfo.newBuilder()
+            .setNormalizedNumber(moduleInfo.getNormalizedNumber())
+            .setCountryIso(moduleInfo.getCountryIso())
+            .setCallType(moduleInfo.getCallType())
+            .setReportingLocation(getReportingLocation())
+            .setContactSource(moduleInfo.getContactSource())
+            .build();
+
+    // For a spam number, add two modules:
+    // (1) "Not spam" and "Block", or
+    // (2) "Not spam" and "Unblock".
+    if (moduleInfo.getIsSpam()) {
+      modules.add(
+          BlockReportSpamModules.moduleForMarkingNumberAsNotSpam(
+              context, blockReportSpamDialogInfo));
+      modules.add(
+          moduleInfo.getIsBlocked()
+              ? BlockReportSpamModules.moduleForUnblockingNumber(context, blockReportSpamDialogInfo)
+              : BlockReportSpamModules.moduleForBlockingNumber(context, blockReportSpamDialogInfo));
+      return this;
+    }
+
+    // For a blocked non-spam number, add the "Unblock" module.
+    if (moduleInfo.getIsBlocked()) {
+      modules.add(
+          BlockReportSpamModules.moduleForUnblockingNumber(context, blockReportSpamDialogInfo));
+      return this;
+    }
+
+    // For a number that is neither a spam number nor blocked, add the "Block/Report spam" module.
+    modules.add(
+        BlockReportSpamModules.moduleForBlockingNumberAndOptionallyReportingSpam(
+            context, blockReportSpamDialogInfo));
+    return this;
+  }
+
+  /**
+   * Adds a module for copying a number.
+   *
+   * <p>The method is a no-op if the number is empty.
+   */
+  public HistoryItemActionModulesBuilder addModuleForCopyingNumber() {
+    if (TextUtils.isEmpty(moduleInfo.getNormalizedNumber())) {
+      return this;
+    }
+
+    modules.add(
+        new HistoryItemActionModule() {
+          @Override
+          public int getStringId() {
+            return R.string.copy_number;
+          }
+
+          @Override
+          public int getDrawableId() {
+            return R.drawable.quantum_ic_content_copy_vd_theme_24;
+          }
+
+          @Override
+          public boolean onClick() {
+            ClipboardUtils.copyText(
+                context,
+                /* label = */ null,
+                moduleInfo.getNormalizedNumber(),
+                /* showToast = */ true);
+            return false;
+          }
+        });
+    return this;
+  }
+
+  private boolean canPlaceCarrierVideoCall() {
+    int carrierVideoAvailability = CallUtil.getVideoCallingAvailability(context);
+    boolean isCarrierVideoCallingEnabled =
+        ((carrierVideoAvailability & CallUtil.VIDEO_CALLING_ENABLED)
+            == CallUtil.VIDEO_CALLING_ENABLED);
+    boolean canRelyOnCarrierVideoPresence =
+        ((carrierVideoAvailability & CallUtil.VIDEO_CALLING_PRESENCE)
+            == CallUtil.VIDEO_CALLING_PRESENCE);
+
+    return isCarrierVideoCallingEnabled
+        && canRelyOnCarrierVideoPresence
+        && moduleInfo.getCanSupportCarrierVideoCall();
+  }
+
+  private boolean isDuoCall() {
+    return DuoComponent.get(context)
+        .getDuo()
+        .isDuoAccount(moduleInfo.getPhoneAccountComponentName());
+  }
+
+  private boolean canPlaceDuoCall() {
+    Duo duo = DuoComponent.get(context).getDuo();
+
+    return duo.isInstalled(context)
+        && duo.isEnabled(context)
+        && duo.isActivated(context)
+        && duo.isReachable(context, moduleInfo.getNormalizedNumber());
+  }
+
+  /**
+   * Lookup URIs are currently fetched from the cached column of the system call log. This URI
+   * contains encoded information for non-contacts for the purposes of populating contact cards.
+   *
+   * <p>We infer whether a contact is existing or not by checking if the lookup URI is "encoded" or
+   * not.
+   *
+   * <p>TODO(zachh): We should revisit this once the contact URI is no longer being read from the
+   * cached column in the system database, in case we decide not to overload the column.
+   */
+  private boolean isExistingContact() {
+    return !TextUtils.isEmpty(moduleInfo.getLookupUri())
+        && !UriUtils.isEncodedContactUri(Uri.parse(moduleInfo.getLookupUri()));
+  }
+
+  /**
+   * Maps the value of {@link HistoryItemActionModuleInfo#getHost()} to {@link
+   * CallInitiationType.Type}, which is required by {@link CallIntentBuilder} to build a call
+   * intent.
+   */
+  private CallInitiationType.Type getCallInitiationType() {
+    switch (moduleInfo.getHost()) {
+      case CALL_LOG:
+        return CallInitiationType.Type.CALL_LOG;
+      case VOICEMAIL:
+        return CallInitiationType.Type.VOICEMAIL_LOG;
+      default:
+        throw Assert.createUnsupportedOperationFailException(
+            String.format("Unsupported host: %s", moduleInfo.getHost()));
+    }
+  }
+
+  /**
+   * Maps the value of {@link HistoryItemActionModuleInfo#getHost()} to {@link
+   * ReportingLocation.Type}, which is for logging where a spam number is reported.
+   */
+  private ReportingLocation.Type getReportingLocation() {
+    switch (moduleInfo.getHost()) {
+      case CALL_LOG:
+        return ReportingLocation.Type.CALL_LOG_HISTORY;
+      case VOICEMAIL:
+        return ReportingLocation.Type.VOICEMAIL_HISTORY;
+      default:
+        throw Assert.createUnsupportedOperationFailException(
+            String.format("Unsupported host: %s", moduleInfo.getHost()));
+    }
+  }
+}
diff --git a/java/com/android/dialer/historyitemactions/IntentModule.java b/java/com/android/dialer/historyitemactions/IntentModule.java
index f73d4c9..dc53064 100644
--- a/java/com/android/dialer/historyitemactions/IntentModule.java
+++ b/java/com/android/dialer/historyitemactions/IntentModule.java
@@ -23,6 +23,7 @@
 import com.android.dialer.callintent.CallIntentBuilder;
 import com.android.dialer.precall.PreCall;
 import com.android.dialer.util.DialerUtils;
+import com.android.dialer.util.IntentUtil;
 
 /**
  * {@link HistoryItemActionModule} useful for making easy to build modules based on starting an
@@ -73,4 +74,12 @@
 
     return new IntentModule(context, PreCall.getIntent(context, callIntentBuilder), text, image);
   }
+
+  public static IntentModule newModuleForSendingTextMessage(Context context, String number) {
+    return new IntentModule(
+        context,
+        IntentUtil.getSendSmsIntent(number),
+        R.string.send_a_message,
+        R.drawable.quantum_ic_message_vd_theme_24);
+  }
 }
diff --git a/java/com/android/dialer/historyitemactions/SharedModules.java b/java/com/android/dialer/historyitemactions/SharedModules.java
deleted file mode 100644
index 8604bed..0000000
--- a/java/com/android/dialer/historyitemactions/SharedModules.java
+++ /dev/null
@@ -1,247 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.dialer.historyitemactions;
-
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-import android.provider.ContactsContract;
-import android.support.annotation.Nullable;
-import android.text.TextUtils;
-import com.android.dialer.DialerPhoneNumber;
-import com.android.dialer.blockreportspam.BlockReportSpamDialogInfo;
-import com.android.dialer.blockreportspam.ShowBlockReportSpamDialogNotifier;
-import com.android.dialer.clipboard.ClipboardUtils;
-import com.android.dialer.util.IntentUtil;
-import com.android.dialer.util.UriUtils;
-import com.google.common.base.Optional;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Modules for the bottom sheet that are shared between NewVoicemailFragment and NewCallLogFragment
- */
-@SuppressWarnings("Guava")
-public class SharedModules {
-
-  public static Optional<HistoryItemActionModule> createModuleForAddingToContacts(
-      Context context,
-      DialerPhoneNumber dialerPhoneNumber,
-      String name,
-      String lookupUri,
-      boolean isBlocked,
-      boolean isSpam) {
-    // Skip showing the menu item for a spam/blocked number.
-    if (isBlocked || isSpam) {
-      return Optional.absent();
-    }
-
-    // Skip showing the menu item for existing contacts.
-    if (isExistingContact(lookupUri)) {
-      return Optional.absent();
-    }
-
-    // Skip showing the menu item if there is no number.
-    String normalizedNumber = dialerPhoneNumber.getNormalizedNumber();
-    if (TextUtils.isEmpty(normalizedNumber)) {
-      return Optional.absent();
-    }
-
-    Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
-    intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
-    intent.putExtra(ContactsContract.Intents.Insert.PHONE, normalizedNumber);
-
-    if (!TextUtils.isEmpty(name)) {
-      intent.putExtra(ContactsContract.Intents.Insert.NAME, name);
-    }
-
-    return Optional.of(
-        new IntentModule(
-            context,
-            intent,
-            R.string.add_to_contacts,
-            R.drawable.quantum_ic_person_add_vd_theme_24));
-  }
-
-  /**
-   * Lookup URIs are currently fetched from the cached column of the system call log. This URI
-   * contains encoded information for non-contacts for the purposes of populating contact cards.
-   *
-   * <p>We infer whether a contact is existing or not by checking if the lookup URI is "encoded" or
-   * not.
-   *
-   * <p>TODO(zachh): We should revisit this once the contact URI is no longer being read from the
-   * cached column in the system database, in case we decide not to overload the column.
-   */
-  private static boolean isExistingContact(@Nullable String lookupUri) {
-    return !TextUtils.isEmpty(lookupUri) && !UriUtils.isEncodedContactUri(Uri.parse(lookupUri));
-  }
-
-  public static Optional<HistoryItemActionModule> createModuleForSendingTextMessage(
-      Context context, String normalizedNumber, boolean isBlocked) {
-    // Don't show the option to send a text message if the number is blocked.
-    if (isBlocked) {
-      return Optional.absent();
-    }
-
-    // TODO(zachh): There are some conditions where this module should not be shown; consider
-    // voicemail, business numbers, etc.
-
-    return !TextUtils.isEmpty(normalizedNumber)
-        ? Optional.of(
-            new IntentModule(
-                context,
-                IntentUtil.getSendSmsIntent(normalizedNumber),
-                R.string.send_a_message,
-                R.drawable.quantum_ic_message_vd_theme_24))
-        : Optional.absent();
-  }
-
-  /**
-   * Create modules related to blocking/unblocking a number and/or reporting it as spam/not spam.
-   */
-  public static List<HistoryItemActionModule> createModulesHandlingBlockedOrSpamNumber(
-      Context context,
-      BlockReportSpamDialogInfo blockReportSpamDialogInfo,
-      boolean isBlocked,
-      boolean isSpam) {
-    List<HistoryItemActionModule> modules = new ArrayList<>();
-
-    // For a spam number, add two options:
-    // (1) "Not spam" and "Block", or
-    // (2) "Not spam" and "Unblock".
-    if (isSpam) {
-      modules.add(createModuleForMarkingNumberAsNonSpam(context, blockReportSpamDialogInfo));
-      modules.add(
-          createModuleForBlockingOrUnblockingNumber(context, blockReportSpamDialogInfo, isBlocked));
-      return modules;
-    }
-
-    // For a blocked non-spam number, add "Unblock" option.
-    if (isBlocked) {
-      modules.add(
-          createModuleForBlockingOrUnblockingNumber(context, blockReportSpamDialogInfo, isBlocked));
-      return modules;
-    }
-
-    // For a number that is neither a spam number nor blocked, add "Block/Report spam" option.
-    modules.add(
-        createModuleForBlockingNumberAndOptionallyReportingSpam(
-            context, blockReportSpamDialogInfo));
-    return modules;
-  }
-
-  /** Create "Not spam" module. */
-  private static HistoryItemActionModule createModuleForMarkingNumberAsNonSpam(
-      Context context, BlockReportSpamDialogInfo blockReportSpamDialogInfo) {
-    return new HistoryItemActionModule() {
-      @Override
-      public int getStringId() {
-        return R.string.not_spam;
-      }
-
-      @Override
-      public int getDrawableId() {
-        return R.drawable.quantum_ic_report_off_vd_theme_24;
-      }
-
-      @Override
-      public boolean onClick() {
-        ShowBlockReportSpamDialogNotifier.notifyShowDialogToReportNotSpam(
-            context, blockReportSpamDialogInfo);
-        return true; // Close the bottom sheet.
-      }
-    };
-  }
-
-  private static HistoryItemActionModule createModuleForBlockingOrUnblockingNumber(
-      Context context, BlockReportSpamDialogInfo blockReportSpamDialogInfo, boolean isBlocked) {
-    return new HistoryItemActionModule() {
-      @Override
-      public int getStringId() {
-        return isBlocked ? R.string.unblock_number : R.string.block_number;
-      }
-
-      @Override
-      public int getDrawableId() {
-        return isBlocked
-            ? R.drawable.quantum_ic_unblock_vd_theme_24
-            : R.drawable.quantum_ic_block_vd_theme_24;
-      }
-
-      @Override
-      public boolean onClick() {
-        if (isBlocked) {
-          ShowBlockReportSpamDialogNotifier.notifyShowDialogToUnblockNumber(
-              context, blockReportSpamDialogInfo);
-        } else {
-          ShowBlockReportSpamDialogNotifier.notifyShowDialogToBlockNumber(
-              context, blockReportSpamDialogInfo);
-        }
-        return true; // Close the bottom sheet.
-      }
-    };
-  }
-
-  /** Create "Block/Report spam" module */
-  private static HistoryItemActionModule createModuleForBlockingNumberAndOptionallyReportingSpam(
-      Context context, BlockReportSpamDialogInfo blockReportSpamDialogInfo) {
-    return new HistoryItemActionModule() {
-      @Override
-      public int getStringId() {
-        return R.string.block_and_optionally_report_spam;
-      }
-
-      @Override
-      public int getDrawableId() {
-        return R.drawable.quantum_ic_block_vd_theme_24;
-      }
-
-      @Override
-      public boolean onClick() {
-        ShowBlockReportSpamDialogNotifier.notifyShowDialogToBlockNumberAndOptionallyReportSpam(
-            context, blockReportSpamDialogInfo);
-        return true; // Close the bottom sheet.
-      }
-    };
-  }
-
-  public static Optional<HistoryItemActionModule> createModuleForCopyingNumber(
-      Context context, String normalizedNumber) {
-    if (TextUtils.isEmpty(normalizedNumber)) {
-      return Optional.absent();
-    }
-    return Optional.of(
-        new HistoryItemActionModule() {
-          @Override
-          public int getStringId() {
-            return R.string.copy_number;
-          }
-
-          @Override
-          public int getDrawableId() {
-            return R.drawable.quantum_ic_content_copy_vd_theme_24;
-          }
-
-          @Override
-          public boolean onClick() {
-            ClipboardUtils.copyText(context, null, normalizedNumber, true);
-            return false;
-          }
-        });
-  }
-}
diff --git a/java/com/android/dialer/historyitemactions/history_item_action_module_info.proto b/java/com/android/dialer/historyitemactions/history_item_action_module_info.proto
new file mode 100644
index 0000000..99071a7
--- /dev/null
+++ b/java/com/android/dialer/historyitemactions/history_item_action_module_info.proto
@@ -0,0 +1,69 @@
+syntax = "proto2";
+
+option java_package = "com.android.dialer.historyitemactions";
+option java_multiple_files = true;
+option optimize_for = LITE_RUNTIME;
+
+
+package com.android.dialer.historyitemactions;
+
+import "java/com/android/dialer/logging/contact_source.proto";
+
+// Contains information needed to construct items (modules) in a bottom sheet.
+// Next ID: 16
+message HistoryItemActionModuleInfo {
+  // The dialer-normalized version of a phone number.
+  // See DialerPhoneNumber.normalized_number.
+  optional string normalized_number = 1;
+
+  // The ISO 3166-1 two letters country code of the number.
+  optional string country_iso = 2;
+
+  // The name associated with the number.
+  optional string name = 3;
+
+  // The type of the call.
+  // See android.provider.CallLog.Calls.TYPE.
+  optional int32 call_type = 4;
+
+  // Bit-mask describing features of the call.
+  // See android.provider.CallLog.Calls.FEATURES.
+  optional int32 features = 5;
+
+  // The Contacts Provider lookup URI for the contact associated with the
+  // number.
+  optional string lookup_uri = 6;
+
+  // The component name of the account used to place or receive the call.
+  // See android.provider.CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME.
+  optional string phone_account_component_name = 7;
+
+  // Whether the number can be reported as invalid through People API
+  optional bool can_report_as_invalid_number = 8;
+
+  // Whether assisted dialing is supported.
+  optional bool can_support_assisted_dialing = 9;
+
+  // Whether carrier video call is supported.
+  optional bool can_support_carrier_video_call = 10;
+
+  // Whether the number is blocked.
+  optional bool is_blocked = 11;
+
+  // Whether the number is spam.
+  optional bool is_spam = 12;
+
+  // Whether the call is to the voicemail inbox.
+  optional bool is_voicemail_call = 13;
+
+  // The source of the contact if there is one associated with the number.
+  optional com.android.dialer.logging.ContactSource.Type contact_source = 14;
+
+  // Places that can host items (modules) in a bottom sheet
+  enum Host {
+    UNKNOWN = 0;
+    CALL_LOG = 1;
+    VOICEMAIL = 2;
+  }
+  optional Host host = 15;
+}
diff --git a/java/com/android/dialer/historyitemactions/history_item_bottom_sheet_header_info.proto b/java/com/android/dialer/historyitemactions/history_item_bottom_sheet_header_info.proto
index ef71ecd..04d9f22 100644
--- a/java/com/android/dialer/historyitemactions/history_item_bottom_sheet_header_info.proto
+++ b/java/com/android/dialer/historyitemactions/history_item_bottom_sheet_header_info.proto
@@ -36,6 +36,4 @@
   //   "Blocked • Mobile • 555-1234", and
   //   "Spam • Mobile • 555-1234".
   optional string secondary_text = 4;
-
-  // TODO(a bug): Add SIM info.
 }
diff --git a/java/com/android/dialer/speeddial/SpeedDialFragment.java b/java/com/android/dialer/speeddial/SpeedDialFragment.java
index db4c024..aa306d2 100644
--- a/java/com/android/dialer/speeddial/SpeedDialFragment.java
+++ b/java/com/android/dialer/speeddial/SpeedDialFragment.java
@@ -29,6 +29,7 @@
 import android.support.v7.app.AppCompatActivity;
 import android.support.v7.widget.RecyclerView;
 import android.support.v7.widget.helper.ItemTouchHelper;
+import android.text.TextUtils;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -46,7 +47,6 @@
 import com.android.dialer.historyitemactions.HistoryItemActionModule;
 import com.android.dialer.historyitemactions.HistoryItemBottomSheetHeaderInfo;
 import com.android.dialer.historyitemactions.IntentModule;
-import com.android.dialer.historyitemactions.SharedModules;
 import com.android.dialer.logging.DialerImpression;
 import com.android.dialer.logging.Logger;
 import com.android.dialer.precall.PreCall;
@@ -60,7 +60,6 @@
 import com.android.dialer.speeddial.loader.SpeedDialUiItem;
 import com.android.dialer.speeddial.loader.UiItemLoaderComponent;
 import com.android.dialer.util.IntentUtil;
-import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.Futures;
 import java.util.ArrayList;
@@ -312,11 +311,9 @@
       }
 
       // Add sms module
-      Optional<HistoryItemActionModule> smsModule =
-          SharedModules.createModuleForSendingTextMessage(
-              getContext(), defaultChannel.number(), false);
-      if (smsModule.isPresent()) {
-        modules.add(smsModule.get());
+      if (!TextUtils.isEmpty(defaultChannel.number())) {
+        modules.add(
+            IntentModule.newModuleForSendingTextMessage(getContext(), defaultChannel.number()));
       }
 
       modules.add(new DividerModule());
diff --git a/java/com/android/dialer/voicemail/listui/menu/Modules.java b/java/com/android/dialer/voicemail/listui/menu/Modules.java
index 226063c..dcd9116 100644
--- a/java/com/android/dialer/voicemail/listui/menu/Modules.java
+++ b/java/com/android/dialer/voicemail/listui/menu/Modules.java
@@ -17,77 +17,53 @@
 package com.android.dialer.voicemail.listui.menu;
 
 import android.content.Context;
-import com.android.dialer.blockreportspam.BlockReportSpamDialogInfo;
-import com.android.dialer.historyitemactions.DividerModule;
+import android.text.TextUtils;
 import com.android.dialer.historyitemactions.HistoryItemActionModule;
-import com.android.dialer.historyitemactions.SharedModules;
-import com.android.dialer.logging.ReportingLocation;
+import com.android.dialer.historyitemactions.HistoryItemActionModuleInfo;
+import com.android.dialer.historyitemactions.HistoryItemActionModulesBuilder;
 import com.android.dialer.voicemail.model.VoicemailEntry;
-import com.google.common.base.Optional;
-import java.util.ArrayList;
 import java.util.List;
 
 /**
  * Configures the modules for the voicemail bottom sheet; these are the rows below the top row
- * (primary action) in the bottom sheet.
+ * (contact info) in the bottom sheet.
  */
-@SuppressWarnings("Guava")
 final class Modules {
 
   static List<HistoryItemActionModule> fromVoicemailEntry(
       Context context, VoicemailEntry voicemailEntry) {
-    // Conditionally add each module, which are items in the bottom sheet's menu.
-    List<HistoryItemActionModule> modules = new ArrayList<>();
+    return new HistoryItemActionModulesBuilder(context, buildModuleInfo(voicemailEntry))
+        // TODO(uabdullah): add module for calls.
+        .addModuleForAddingToContacts()
+        .addModuleForSendingTextMessage()
+        .addModuleForDivider()
+        .addModuleForBlockedOrSpamNumber()
+        .addModuleForCopyingNumber()
+        // TODO(zachh): Module for CallComposer.
+        .build();
+  }
 
-    // TODO(uabdullah): Handle maybeAddModuleForVideoOrAudioCall(context, modules, row);
-    Optional<HistoryItemActionModule> moduleForAddingContacts =
-        SharedModules.createModuleForAddingToContacts(
-            context,
-            voicemailEntry.getNumber(),
-            voicemailEntry.getNumberAttributes().getName(),
-            voicemailEntry.getNumberAttributes().getLookupUri(),
-            voicemailEntry.getNumberAttributes().getIsBlocked(),
-            voicemailEntry.getNumberAttributes().getIsSpam());
-    if (moduleForAddingContacts.isPresent()) {
-      modules.add(moduleForAddingContacts.get());
-    }
-
-    Optional<HistoryItemActionModule> moduleForSendingTextMessage =
-        SharedModules.createModuleForSendingTextMessage(
-            context,
-            voicemailEntry.getNumber().getNormalizedNumber(),
-            voicemailEntry.getNumberAttributes().getIsBlocked());
-    if (moduleForSendingTextMessage.isPresent()) {
-      modules.add(moduleForSendingTextMessage.get());
-    }
-
-    if (!modules.isEmpty()) {
-      modules.add(new DividerModule());
-    }
-
-    BlockReportSpamDialogInfo blockReportSpamDialogInfo =
-        BlockReportSpamDialogInfo.newBuilder()
-            .setNormalizedNumber(voicemailEntry.getNumber().getNormalizedNumber())
-            .setCountryIso(voicemailEntry.getNumber().getCountryIso())
-            .setCallType(voicemailEntry.getCallType())
-            .setReportingLocation(ReportingLocation.Type.VOICEMAIL_HISTORY)
-            .setContactSource(voicemailEntry.getNumberAttributes().getContactSource())
-            .build();
-    modules.addAll(
-        SharedModules.createModulesHandlingBlockedOrSpamNumber(
-            context,
-            blockReportSpamDialogInfo,
-            voicemailEntry.getNumberAttributes().getIsBlocked(),
-            voicemailEntry.getNumberAttributes().getIsSpam()));
-
-    // TODO(zachh): Module for CallComposer.
-    Optional<HistoryItemActionModule> moduleForCopyingNumber =
-        SharedModules.createModuleForCopyingNumber(
-            context, voicemailEntry.getNumber().getNormalizedNumber());
-    if (moduleForCopyingNumber.isPresent()) {
-      modules.add(moduleForCopyingNumber.get());
-    }
-
-    return modules;
+  private static HistoryItemActionModuleInfo buildModuleInfo(VoicemailEntry voicemailEntry) {
+    return HistoryItemActionModuleInfo.newBuilder()
+        .setNormalizedNumber(voicemailEntry.getNumber().getNormalizedNumber())
+        .setCountryIso(voicemailEntry.getNumber().getCountryIso())
+        .setName(voicemailEntry.getNumberAttributes().getName())
+        .setCallType(voicemailEntry.getCallType())
+        .setLookupUri(voicemailEntry.getNumberAttributes().getLookupUri())
+        .setPhoneAccountComponentName(voicemailEntry.getPhoneAccountComponentName())
+        .setCanReportAsInvalidNumber(
+            voicemailEntry.getNumberAttributes().getCanReportAsInvalidNumber())
+        .setCanSupportAssistedDialing(
+            !TextUtils.isEmpty(voicemailEntry.getNumberAttributes().getLookupUri()))
+        .setCanSupportCarrierVideoCall(
+            voicemailEntry.getNumberAttributes().getCanSupportCarrierVideoCall())
+        .setIsBlocked(voicemailEntry.getNumberAttributes().getIsBlocked())
+        .setIsSpam(voicemailEntry.getNumberAttributes().getIsSpam())
+        // A voicemail call is an outgoing call to the voicemail box.
+        // Voicemail entries are not voicemail calls.
+        .setIsVoicemailCall(false)
+        .setContactSource(voicemailEntry.getNumberAttributes().getContactSource())
+        .setHost(HistoryItemActionModuleInfo.Host.VOICEMAIL)
+        .build();
   }
 }
diff --git a/java/com/android/incallui/InCallActivity.java b/java/com/android/incallui/InCallActivity.java
index 5ac6b50..98f0019 100644
--- a/java/com/android/incallui/InCallActivity.java
+++ b/java/com/android/incallui/InCallActivity.java
@@ -1504,7 +1504,8 @@
             call.getVideoTech().isSelfManagedCamera(),
             shouldAllowAnswerAndRelease(call),
             CallList.getInstance().getBackgroundCall() != null,
-            call.isSpeakEasyEligible());
+            getSpeakEasyCallManager().isAvailable(getApplicationContext())
+                && call.isSpeakEasyEligible());
     transaction.add(R.id.main, answerScreen.getAnswerScreenFragment(), Tags.ANSWER_SCREEN);
 
     Logger.get(this).logScreenView(ScreenEvent.Type.INCOMING_CALL, this);
diff --git a/java/com/android/incallui/InCallServiceImpl.java b/java/com/android/incallui/InCallServiceImpl.java
index d803956..b9d0ecc 100644
--- a/java/com/android/incallui/InCallServiceImpl.java
+++ b/java/com/android/incallui/InCallServiceImpl.java
@@ -26,7 +26,6 @@
 import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
 import com.android.dialer.feedback.FeedbackComponent;
 import com.android.incallui.audiomode.AudioModeProvider;
-import com.android.incallui.audiomode.BluetoothDeviceProviderComponent;
 import com.android.incallui.call.CallList;
 import com.android.incallui.call.ExternalCallList;
 import com.android.incallui.call.TelecomAdapter;
@@ -98,7 +97,6 @@
     final Context context = getApplicationContext();
     final ContactInfoCache contactInfoCache = ContactInfoCache.getInstance(context);
     AudioModeProvider.getInstance().initializeAudioState(this);
-    BluetoothDeviceProviderComponent.get(context).bluetoothDeviceProvider().setUp();
     InCallPresenter.getInstance()
         .setUp(
             context,
@@ -142,7 +140,6 @@
     // Tear down the InCall system
     InCallPresenter.getInstance().tearDown();
     TelecomAdapter.getInstance().clearInCallService();
-    BluetoothDeviceProviderComponent.get(this).bluetoothDeviceProvider().tearDown();
     if (returnToCallController != null) {
       returnToCallController.tearDown();
       returnToCallController = null;
diff --git a/java/com/android/incallui/audiomode/BluetoothDeviceProvider.java b/java/com/android/incallui/audiomode/BluetoothDeviceProvider.java
deleted file mode 100644
index 1aa1c20..0000000
--- a/java/com/android/incallui/audiomode/BluetoothDeviceProvider.java
+++ /dev/null
@@ -1,203 +0,0 @@
-/*
- * Copyright (C) 2018 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.audiomode;
-
-import android.annotation.SuppressLint;
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothHeadset;
-import android.bluetooth.BluetoothProfile;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.util.ArraySet;
-import com.android.dialer.common.LogUtil;
-import com.android.dialer.inject.ApplicationContext;
-import java.lang.reflect.Method;
-import java.util.List;
-import java.util.Set;
-import javax.inject.Inject;
-import javax.inject.Singleton;
-
-/** Proxy class for getting and setting connected/active Bluetooth devices. */
-@Singleton
-public final class BluetoothDeviceProvider extends BroadcastReceiver {
-
-  // TODO(yueg): use BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED when possible
-  private static final String ACTION_ACTIVE_DEVICE_CHANGED =
-      "android.bluetooth.headset.profile.action.ACTIVE_DEVICE_CHANGED";
-
-  private final Context appContext;
-  private final BluetoothProfileServiceListener bluetoothProfileServiceListener =
-      new BluetoothProfileServiceListener();
-
-  private final Set<BluetoothDevice> connectedBluetoothDeviceSet = new ArraySet<>();
-
-  private BluetoothDevice activeBluetoothDevice;
-  private BluetoothHeadset bluetoothHeadset;
-  private boolean isSetUp;
-
-  @Inject
-  public BluetoothDeviceProvider(@ApplicationContext Context appContext) {
-    this.appContext = appContext;
-  }
-
-  public void setUp() {
-    if (BluetoothAdapter.getDefaultAdapter() == null) {
-      // Bluetooth is not supported on this hardware platform
-      return;
-    }
-    // Get Bluetooth service including the initial connected device list (should only contain one
-    // device)
-    BluetoothAdapter.getDefaultAdapter()
-        .getProfileProxy(appContext, bluetoothProfileServiceListener, BluetoothProfile.HEADSET);
-    // Get notified of Bluetooth device update
-    IntentFilter filter = new IntentFilter();
-    filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
-    filter.addAction(ACTION_ACTIVE_DEVICE_CHANGED);
-    appContext.registerReceiver(this, filter);
-
-    isSetUp = true;
-  }
-
-  public void tearDown() {
-    if (!isSetUp) {
-      return;
-    }
-    appContext.unregisterReceiver(this);
-    if (bluetoothHeadset != null) {
-      BluetoothAdapter.getDefaultAdapter()
-          .closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset);
-    }
-  }
-
-  public Set<BluetoothDevice> getConnectedBluetoothDeviceSet() {
-    return connectedBluetoothDeviceSet;
-  }
-
-  public BluetoothDevice getActiveBluetoothDevice() {
-    return activeBluetoothDevice;
-  }
-
-  @SuppressLint("PrivateApi")
-  public void setActiveBluetoothDevice(BluetoothDevice bluetoothDevice) {
-    if (!connectedBluetoothDeviceSet.contains(bluetoothDevice)) {
-      LogUtil.e("BluetoothProfileServiceListener.setActiveBluetoothDevice", "device is not in set");
-      return;
-    }
-    // TODO(yueg): use BluetoothHeadset.setActiveDevice() when possible
-    try {
-      Method getActiveDeviceMethod =
-          bluetoothHeadset.getClass().getDeclaredMethod("setActiveDevice", BluetoothDevice.class);
-      getActiveDeviceMethod.setAccessible(true);
-      getActiveDeviceMethod.invoke(bluetoothHeadset, bluetoothDevice);
-    } catch (Exception e) {
-      LogUtil.e(
-          "BluetoothProfileServiceListener.setActiveBluetoothDevice",
-          "failed to call setActiveDevice",
-          e);
-    }
-  }
-
-  @Override
-  public void onReceive(Context context, Intent intent) {
-    if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) {
-      handleActionConnectionStateChanged(intent);
-    } else if (ACTION_ACTIVE_DEVICE_CHANGED.equals(intent.getAction())) {
-      handleActionActiveDeviceChanged(intent);
-    }
-  }
-
-  private void handleActionConnectionStateChanged(Intent intent) {
-    if (!intent.hasExtra(BluetoothDevice.EXTRA_DEVICE)) {
-      LogUtil.i(
-          "BluetoothDeviceProvider.handleActionConnectionStateChanged",
-          "extra BluetoothDevice.EXTRA_DEVICE not found");
-      return;
-    }
-    BluetoothDevice bluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
-    if (bluetoothDevice == null) {
-      return;
-    }
-
-    int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
-    if (state == BluetoothProfile.STATE_DISCONNECTED) {
-      connectedBluetoothDeviceSet.remove(bluetoothDevice);
-      LogUtil.i("BluetoothDeviceProvider.handleActionConnectionStateChanged", "device removed");
-    } else if (state == BluetoothProfile.STATE_CONNECTED) {
-      connectedBluetoothDeviceSet.add(bluetoothDevice);
-      LogUtil.i("BluetoothDeviceProvider.handleActionConnectionStateChanged", "device added");
-    }
-  }
-
-  private void handleActionActiveDeviceChanged(Intent intent) {
-    if (!intent.hasExtra(BluetoothDevice.EXTRA_DEVICE)) {
-      LogUtil.i(
-          "BluetoothDeviceProvider.handleActionActiveDeviceChanged",
-          "extra BluetoothDevice.EXTRA_DEVICE not found");
-      return;
-    }
-    activeBluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
-    LogUtil.i(
-        "BluetoothDeviceProvider.handleActionActiveDeviceChanged",
-        (activeBluetoothDevice == null ? "null" : ""));
-  }
-
-  private final class BluetoothProfileServiceListener implements BluetoothProfile.ServiceListener {
-    @Override
-    @SuppressLint("PrivateApi")
-    public void onServiceConnected(int profile, BluetoothProfile bluetoothProfile) {
-      if (profile != BluetoothProfile.HEADSET) {
-        return;
-      }
-      // Get initial connected device list
-      bluetoothHeadset = (BluetoothHeadset) bluetoothProfile;
-      List<BluetoothDevice> devices = bluetoothProfile.getConnectedDevices();
-      for (BluetoothDevice device : devices) {
-        connectedBluetoothDeviceSet.add(device);
-        LogUtil.i(
-            "BluetoothProfileServiceListener.onServiceConnected", "get initial connected device");
-      }
-
-      // Get initial active device
-      // TODO(yueg): use BluetoothHeadset.getActiveDevice() when possible
-      try {
-        Method getActiveDeviceMethod =
-            bluetoothHeadset.getClass().getDeclaredMethod("getActiveDevice");
-        getActiveDeviceMethod.setAccessible(true);
-        activeBluetoothDevice = (BluetoothDevice) getActiveDeviceMethod.invoke(bluetoothHeadset);
-        LogUtil.i(
-            "BluetoothProfileServiceListener.onServiceConnected",
-            "get initial active device" + ((activeBluetoothDevice == null) ? " null" : ""));
-      } catch (Exception e) {
-        LogUtil.e(
-            "BluetoothProfileServiceListener.onServiceConnected",
-            "failed to call getAcitveDevice",
-            e);
-      }
-    }
-
-    @Override
-    public void onServiceDisconnected(int profile) {
-      LogUtil.enterBlock("BluetoothProfileServiceListener.onServiceDisconnected");
-      if (profile == BluetoothProfile.HEADSET) {
-        bluetoothHeadset = null;
-      }
-    }
-  }
-}
diff --git a/java/com/android/incallui/audiomode/BluetoothDeviceProviderComponent.java b/java/com/android/incallui/audiomode/BluetoothDeviceProviderComponent.java
deleted file mode 100644
index 9cd9268..0000000
--- a/java/com/android/incallui/audiomode/BluetoothDeviceProviderComponent.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.incallui.audiomode;
-
-import android.content.Context;
-import com.android.dialer.inject.HasRootComponent;
-import dagger.Subcomponent;
-
-/** Dagger component for the Bluetooth device provider. */
-@Subcomponent
-public abstract class BluetoothDeviceProviderComponent {
-
-  public abstract BluetoothDeviceProvider bluetoothDeviceProvider();
-
-  public static BluetoothDeviceProviderComponent get(Context context) {
-    return ((BluetoothDeviceProviderComponent.HasComponent)
-            ((HasRootComponent) context.getApplicationContext()).component())
-        .bluetoothDeviceProviderComponent();
-  }
-
-  /** Used to refer to the root application component. */
-  public interface HasComponent {
-    BluetoothDeviceProviderComponent bluetoothDeviceProviderComponent();
-  }
-}
diff --git a/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java b/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java
index d6946d8..a561b5e 100644
--- a/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java
+++ b/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java
@@ -39,12 +39,12 @@
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.logging.DialerImpression;
 import com.android.dialer.logging.Logger;
-import com.android.incallui.audiomode.BluetoothDeviceProviderComponent;
 import com.android.incallui.call.CallList;
 import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.TelecomAdapter;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
-import java.util.Set;
+import java.util.Collection;
 
 /** Shows picker for audio routes */
 public class AudioRouteSelectorDialogFragment extends BottomSheetDialogFragment {
@@ -91,24 +91,33 @@
 
   @Nullable
   @Override
+  @SuppressLint("NewApi")
   public View onCreateView(
       LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
     View view = layoutInflater.inflate(R.layout.audioroute_selector, viewGroup, false);
     CallAudioState audioState = getArguments().getParcelable(ARG_AUDIO_STATE);
 
-    Set<BluetoothDevice> bluetoothDeviceSet =
-        BluetoothDeviceProviderComponent.get(getContext())
-            .bluetoothDeviceProvider()
-            .getConnectedBluetoothDeviceSet();
-    for (BluetoothDevice device : bluetoothDeviceSet) {
-      boolean selected =
-          (audioState.getRoute() == CallAudioState.ROUTE_BLUETOOTH)
-              && (bluetoothDeviceSet.size() == 1
-                  || device.equals(
-                      BluetoothDeviceProviderComponent.get(getContext())
-                          .bluetoothDeviceProvider()
-                          .getActiveBluetoothDevice()));
-      TextView textView = createBluetoothItem(device, selected);
+    if (BuildCompat.isAtLeastP()) {
+      // Create items for all connected Bluetooth devices
+      Collection<BluetoothDevice> bluetoothDeviceSet = audioState.getSupportedBluetoothDevices();
+      for (BluetoothDevice device : bluetoothDeviceSet) {
+        boolean selected =
+            (audioState.getRoute() == CallAudioState.ROUTE_BLUETOOTH)
+                && (bluetoothDeviceSet.size() == 1
+                    || device.equals(audioState.getActiveBluetoothDevice()));
+        TextView textView = createBluetoothItem(device, selected);
+        ((LinearLayout) view).addView(textView, 0);
+      }
+    } else {
+      // Only create Bluetooth audio route
+      TextView textView =
+          (TextView) getLayoutInflater().inflate(R.layout.audioroute_item, null, false);
+      textView.setText(getString(R.string.audioroute_bluetooth));
+      initItem(
+          textView,
+          CallAudioState.ROUTE_BLUETOOTH,
+          audioState,
+          DialerImpression.Type.IN_CALL_SWITCH_AUDIO_ROUTE_BLUETOOTH);
       ((LinearLayout) view).addView(textView, 0);
     }
 
@@ -183,9 +192,7 @@
                   AudioRouteSelectorDialogFragment.this, AudioRouteSelectorPresenter.class)
               .onAudioRouteSelected(CallAudioState.ROUTE_BLUETOOTH);
           // Set active Bluetooth device
-          BluetoothDeviceProviderComponent.get(getContext())
-              .bluetoothDeviceProvider()
-              .setActiveBluetoothDevice(bluetoothDevice);
+          TelecomAdapter.getInstance().requestBluetoothAudio(bluetoothDevice);
           dismiss();
         });
 
diff --git a/java/com/android/incallui/call/DialerCall.java b/java/com/android/incallui/call/DialerCall.java
index 1a0de19..77e2ea3 100644
--- a/java/com/android/incallui/call/DialerCall.java
+++ b/java/com/android/incallui/call/DialerCall.java
@@ -50,7 +50,6 @@
 import android.text.TextUtils;
 import android.widget.Toast;
 import com.android.contacts.common.compat.CallCompat;
-import com.android.contacts.common.compat.telecom.TelecomManagerCompat;
 import com.android.dialer.assisteddialing.ConcreteCreator;
 import com.android.dialer.assisteddialing.TransformationInfo;
 import com.android.dialer.callintent.CallInitiationType;
@@ -86,7 +85,6 @@
 import com.android.incallui.call.state.DialerCallState;
 import com.android.incallui.latencyreport.LatencyReport;
 import com.android.incallui.rtt.protocol.RttChatMessage;
-import com.android.incallui.speakeasy.runtime.Constraints;
 import com.android.incallui.videotech.VideoTech;
 import com.android.incallui.videotech.VideoTech.VideoTechListener;
 import com.android.incallui.videotech.duo.DuoVideoTech;
@@ -118,8 +116,11 @@
   public static final int PROPERTY_CODEC_KNOWN = 0x04000000;
 
   private static final String ID_PREFIX = "DialerCall_";
-  private static final String CONFIG_EMERGENCY_CALLBACK_WINDOW_MILLIS =
+
+  @VisibleForTesting
+  public static final String CONFIG_EMERGENCY_CALLBACK_WINDOW_MILLIS =
       "emergency_callback_window_millis";
+
   private static int idCounter = 0;
 
   /**
@@ -822,10 +823,9 @@
     // We want to treat any incoming call that arrives a short time after an outgoing emergency call
     // as a potential emergency callback.
     if (getExtras() != null
-        && getExtras().getLong(TelecomManagerCompat.EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS, 0)
-            > 0) {
+        && getExtras().getLong(Call.EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS, 0) > 0) {
       long lastEmergencyCallMillis =
-          getExtras().getLong(TelecomManagerCompat.EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS, 0);
+          getExtras().getLong(Call.EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS, 0);
       if (isInEmergencyCallbackWindow(lastEmergencyCallMillis)) {
         return true;
       }
@@ -1058,6 +1058,7 @@
   }
 
   @TargetApi(28)
+  @Nullable
   public RttCall getRttCall() {
     if (!isActiveRttCall()) {
       return null;
@@ -1111,16 +1112,18 @@
     if (!BuildCompat.isAtLeastP()) {
       return;
     }
-    // Save any remaining text in the buffer that's not shown by UI yet.
-    // This may happen when the call is switched to background before disconnect.
-    try {
-      String messageLeft = getRttCall().readImmediately();
-      if (!TextUtils.isEmpty(messageLeft)) {
-        rttTranscript =
-            RttChatMessage.getRttTranscriptWithNewRemoteMessage(rttTranscript, messageLeft);
+    if (getRttCall() != null) {
+      // Save any remaining text in the buffer that's not shown by UI yet.
+      // This may happen when the call is switched to background before disconnect.
+      try {
+        String messageLeft = getRttCall().readImmediately();
+        if (!TextUtils.isEmpty(messageLeft)) {
+          rttTranscript =
+              RttChatMessage.getRttTranscriptWithNewRemoteMessage(rttTranscript, messageLeft);
+        }
+      } catch (IOException e) {
+        LogUtil.e("DialerCall.saveRttTranscript", "error when reading remaining message", e);
       }
-    } catch (IOException e) {
-      LogUtil.e("DialerCall.saveRttTranscript", "error when reading remaining message", e);
     }
     // Don't save transcript if it's empty.
     if (rttTranscript.getMessagesCount() == 0) {
@@ -1662,7 +1665,6 @@
     if (videoTechManager != null) {
       videoTechManager.dispatchRemovedFromCallList();
     }
-    // TODO(a bug): Add tests for it to make sure no crash on subsequent call to this method.
     // TODO(wangqi): Consider moving this to a DialerCallListener.
     if (rttTranscript != null && !isCallRemoved) {
       saveRttTranscript();
@@ -1697,10 +1699,6 @@
 
   /** Indicates the call is eligible for SpeakEasy */
   public boolean isSpeakEasyEligible() {
-    if (!Constraints.isAvailable(context)) {
-      return false;
-    }
-
     return !isPotentialEmergencyCallback()
         && !isEmergencyCall()
         && !isActiveRttCall()
diff --git a/java/com/android/incallui/call/TelecomAdapter.java b/java/com/android/incallui/call/TelecomAdapter.java
index a7e10d3..4ae1bc1 100644
--- a/java/com/android/incallui/call/TelecomAdapter.java
+++ b/java/com/android/incallui/call/TelecomAdapter.java
@@ -16,7 +16,9 @@
 
 package com.android.incallui.call;
 
+import android.annotation.TargetApi;
 import android.app.Notification;
+import android.bluetooth.BluetoothDevice;
 import android.content.ActivityNotFoundException;
 import android.content.Intent;
 import android.os.Looper;
@@ -193,4 +195,13 @@
           "no inCallService available for stopping foreground notification");
     }
   }
+
+  @TargetApi(28)
+  public void requestBluetoothAudio(BluetoothDevice bluetoothDevice) {
+    if (inCallService != null) {
+      inCallService.requestBluetoothAudio(bluetoothDevice);
+    } else {
+      LogUtil.e("TelecomAdapter.requestBluetoothAudio", "inCallService is null");
+    }
+  }
 }
diff --git a/java/com/android/incallui/rtt/impl/AudioSelectMenu.java b/java/com/android/incallui/rtt/impl/AudioSelectMenu.java
index 01c3950..1c83637 100644
--- a/java/com/android/incallui/rtt/impl/AudioSelectMenu.java
+++ b/java/com/android/incallui/rtt/impl/AudioSelectMenu.java
@@ -17,8 +17,6 @@
 package com.android.incallui.rtt.impl;
 
 import android.content.Context;
-import android.content.res.ColorStateList;
-import android.graphics.PorterDuff.Mode;
 import android.telecom.CallAudioState;
 import android.view.View;
 import android.widget.PopupWindow;
@@ -28,8 +26,11 @@
 public class AudioSelectMenu extends PopupWindow {
 
   private final InCallButtonUiDelegate inCallButtonUiDelegate;
-  private final Context context;
   private final OnButtonClickListener onButtonClickListener;
+  private final RttCheckableButton bluetoothButton;
+  private final RttCheckableButton speakerButton;
+  private final RttCheckableButton headsetButton;
+  private final RttCheckableButton earpieceButton;
 
   interface OnButtonClickListener {
     void onBackPressed();
@@ -40,7 +41,6 @@
       InCallButtonUiDelegate inCallButtonUiDelegate,
       OnButtonClickListener onButtonClickListener) {
     super(context, null, 0, R.style.OverflowMenu);
-    this.context = context;
     this.inCallButtonUiDelegate = inCallButtonUiDelegate;
     this.onButtonClickListener = onButtonClickListener;
     View view = View.inflate(context, R.layout.audio_route, null);
@@ -55,28 +55,32 @@
               this.onButtonClickListener.onBackPressed();
             });
     CallAudioState audioState = inCallButtonUiDelegate.getCurrentAudioState();
-    initItem(
-        view.findViewById(R.id.audioroute_bluetooth), CallAudioState.ROUTE_BLUETOOTH, audioState);
-    initItem(view.findViewById(R.id.audioroute_speaker), CallAudioState.ROUTE_SPEAKER, audioState);
-    initItem(
-        view.findViewById(R.id.audioroute_headset), CallAudioState.ROUTE_WIRED_HEADSET, audioState);
-    initItem(
-        view.findViewById(R.id.audioroute_earpiece), CallAudioState.ROUTE_EARPIECE, audioState);
+    bluetoothButton = view.findViewById(R.id.audioroute_bluetooth);
+    speakerButton = view.findViewById(R.id.audioroute_speaker);
+    headsetButton = view.findViewById(R.id.audioroute_headset);
+    earpieceButton = view.findViewById(R.id.audioroute_earpiece);
+    initItem(bluetoothButton, CallAudioState.ROUTE_BLUETOOTH, audioState);
+    initItem(speakerButton, CallAudioState.ROUTE_SPEAKER, audioState);
+    initItem(headsetButton, CallAudioState.ROUTE_WIRED_HEADSET, audioState);
+    initItem(earpieceButton, CallAudioState.ROUTE_EARPIECE, audioState);
   }
 
   private void initItem(RttCheckableButton item, final int itemRoute, CallAudioState audioState) {
-    int selectedColor =
-        context.getColor(com.android.incallui.audioroute.R.color.dialer_theme_color);
     if ((audioState.getSupportedRouteMask() & itemRoute) == 0) {
       item.setVisibility(View.GONE);
     } else if (audioState.getRoute() == itemRoute) {
-      item.setTextColor(selectedColor);
-      item.setCompoundDrawableTintList(ColorStateList.valueOf(selectedColor));
-      item.setCompoundDrawableTintMode(Mode.SRC_ATOP);
+      item.setChecked(true);
     }
     item.setOnClickListener(
         (v) -> {
           inCallButtonUiDelegate.setAudioRoute(itemRoute);
         });
   }
+
+  void setAudioState(CallAudioState audioState) {
+    bluetoothButton.setChecked(audioState.getRoute() == CallAudioState.ROUTE_BLUETOOTH);
+    speakerButton.setChecked(audioState.getRoute() == CallAudioState.ROUTE_SPEAKER);
+    headsetButton.setChecked(audioState.getRoute() == CallAudioState.ROUTE_WIRED_HEADSET);
+    earpieceButton.setChecked(audioState.getRoute() == CallAudioState.ROUTE_EARPIECE);
+  }
 }
diff --git a/java/com/android/incallui/rtt/impl/RttChatFragment.java b/java/com/android/incallui/rtt/impl/RttChatFragment.java
index e567159..c393393 100644
--- a/java/com/android/incallui/rtt/impl/RttChatFragment.java
+++ b/java/com/android/incallui/rtt/impl/RttChatFragment.java
@@ -107,6 +107,7 @@
   private PrimaryCallState primaryCallState = PrimaryCallState.empty();
   private boolean isUserScrolling;
   private boolean shouldAutoScrolling;
+  private AudioSelectMenu audioSelectMenu;
 
   /**
    * Create a new instance of RttChatFragment.
@@ -558,6 +559,9 @@
     LogUtil.i("RttChatFragment.setAudioState", "audioState: " + audioState);
     overflowMenu.setMuteButtonChecked(audioState.isMuted());
     overflowMenu.setAudioState(audioState);
+    if (audioSelectMenu != null) {
+      audioSelectMenu.setAudioState(audioState);
+    }
   }
 
   @Override
@@ -573,7 +577,7 @@
 
   @Override
   public void showAudioRouteSelector() {
-    AudioSelectMenu audioSelectMenu =
+    audioSelectMenu =
         new AudioSelectMenu(
             getContext(),
             inCallButtonUiDelegate,
diff --git a/java/com/android/incallui/speakeasy/SpeakEasyCallManager.java b/java/com/android/incallui/speakeasy/SpeakEasyCallManager.java
index f2721da..8a815d3 100644
--- a/java/com/android/incallui/speakeasy/SpeakEasyCallManager.java
+++ b/java/com/android/incallui/speakeasy/SpeakEasyCallManager.java
@@ -16,6 +16,7 @@
 
 package com.android.incallui.speakeasy;
 
+import android.content.Context;
 import android.support.annotation.NonNull;
 import android.support.v4.app.Fragment;
 import com.android.incallui.call.DialerCall;
@@ -37,4 +38,15 @@
    * @param call The call which has been removed.
    */
   void onCallRemoved(@NonNull DialerCall call);
+
+  /**
+   * Indicates the feature is available.
+   *
+   * @param context The application context.
+   */
+  boolean isAvailable(@NonNull Context context);
+
+  /** Returns the config provider flag associated with the feature. */
+  @NonNull
+  String getConfigProviderFlag();
 }
diff --git a/java/com/android/incallui/speakeasy/SpeakEasyCallManagerStub.java b/java/com/android/incallui/speakeasy/SpeakEasyCallManagerStub.java
index 9e58ce1..a040973 100644
--- a/java/com/android/incallui/speakeasy/SpeakEasyCallManagerStub.java
+++ b/java/com/android/incallui/speakeasy/SpeakEasyCallManagerStub.java
@@ -16,6 +16,8 @@
 
 package com.android.incallui.speakeasy;
 
+import android.content.Context;
+import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.v4.app.Fragment;
 import com.android.incallui.call.DialerCall;
@@ -38,4 +40,17 @@
   /** Always inert in the stub. */
   @Override
   public void onCallRemoved(DialerCall call) {}
+
+  /** Always returns false. */
+  @Override
+  public boolean isAvailable(@NonNull Context unused) {
+    return false;
+  }
+
+  /** Always returns a stub string. */
+  @NonNull
+  @Override
+  public String getConfigProviderFlag() {
+    return "not_yet_implmented";
+  }
 }
diff --git a/java/com/android/incallui/speakeasy/runtime/Constraints.java b/java/com/android/incallui/speakeasy/runtime/Constraints.java
deleted file mode 100644
index 1206d59..0000000
--- a/java/com/android/incallui/speakeasy/runtime/Constraints.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2018 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.speakeasy.runtime;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.os.Build.VERSION_CODES;
-import android.support.annotation.NonNull;
-import android.support.annotation.VisibleForTesting;
-import android.support.v4.os.BuildCompat;
-import android.support.v4.os.UserManagerCompat;
-import com.android.dialer.common.Assert;
-import com.android.dialer.common.LogUtil;
-import com.android.dialer.configprovider.ConfigProviderBindings;
-import com.android.dialer.util.PermissionsUtil;
-
-/** Preconditions for the use of SpeakEasyModule */
-public final class Constraints {
-
-  @VisibleForTesting public static final String SPEAK_EASY_ENABLED = "speak_easy_enabled";
-  private static final String[] REQUIRED_PERMISSIONS = {
-
-  };
-
-  // Non-instantiatable.
-  private Constraints() {}
-
-  public static boolean isAvailable(@NonNull Context context) {
-    Assert.isNotNull(context);
-
-    return isServerConfigEnabled(context)
-        && isUserUnlocked(context)
-        && meetsPlatformSdkFloor()
-        && hasNecessaryPermissions(context);
-  }
-
-  private static boolean isServerConfigEnabled(@NonNull Context context) {
-    return ConfigProviderBindings.get(context).getBoolean(SPEAK_EASY_ENABLED, false);
-  }
-
-  private static boolean isUserUnlocked(@NonNull Context context) {
-    return UserManagerCompat.isUserUnlocked(context);
-  }
-
-  private static boolean meetsPlatformSdkFloor() {
-    return BuildCompat.isAtLeastP();
-  }
-
-  @SuppressWarnings("AndroidApiChecker") // Use of Java 8 APIs.
-  @TargetApi(VERSION_CODES.N)
-  private static boolean hasNecessaryPermissions(@NonNull Context context) {
-    for (String permission : REQUIRED_PERMISSIONS) {
-      if (!PermissionsUtil.hasPermission(context, permission)) {
-        LogUtil.i("Constraints.hasNecessaryPermissions", "missing permission: %s ", permission);
-        return false;
-      }
-    }
-    return true;
-  }
-}