Merge changes Ibab0bc7e,I1cb26187,I64225b78,I4806ab65,I7d431e74, ...

* changes:
  Remove TODOs for showing SIM info in the bottom sheet & call details.
  Use Telecom Bluetooth API instead of system Bluetooth API.
  Update audio route after user select different audio route.
  Clear NewCallLogViewHolder.onClickListener if row is not callable.
  Filter out unnecessary bottom sheet options for a call to a voicemail box.
  Aosp fix for v28-support-prelease bottom sheet.
  Mark photo info as voicemails in bottom sheet
  Simplify how we build bottom sheet options (a.k.a. modules).
  Add test to verify no crash on multiple DialerCall#onRemovedFromCallList.
  Add spam status tests for CallList#onCallAdded
  More refactoring
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;
-  }
-}