Merge changes Ia54e3421,Id2176e6e,I0311770e,I79f99c34,I8579afff, ...

* changes:
  Add some annotations that won't influence aosp.
  Update answer button logic.
  Hide new after call spam blocking promo behind an additional flag.
  Allow the TextView for call log primary text to adjust size when recycled.
  Format callback phone number.
  Added getLoggingName() to CallLogDataSource and PhoneLookup interfaces.
  Do not show bubble for outgoing call if it's not a background call.
  Always fetch status onResume and add logging for voicemail status in OldMainPeer and
  Place Duo calls with PreCall
  Creating CallIntent, AutoValue builder, to replace CallIntentBuilder.
  Set the DisplayNameSource to PHONE in DefaultLookupUriGenerator.
  Config correct layout boundaries to accommodate long text (call log & bottom sheet)
  Use info from EmergencyPhoneLookup to render UI for an emergency number.
  Implement EmergencyPhoneLookup for checking if a number is an emergency number.
  Add GlobalSpamListStatus and UserSpamListStatus
  Move SpamStatus classes into subpackage
  Delete obsolete checkSpamStatus(Listener) API
  Update callers of checkSpamStatus to use Future based API
  Fix bug that showing block option for private number.
  Add a null check for digitsHint.
  Don't commit fragment transactions if it's not safe.
  Add ListenableFuture based APIs for checkSpamStatus
  Pass activity between new call log's adapter/view holder.
  Replace assert checks with safety checks instead.
  Add SimpleSpamStatus and use it in FakeSpam and SpamStub
  Show calls to/from emergency numbers as "Emergency number" in call log & call details
diff --git a/java/com/android/dialer/app/calllog/CallLogAdapter.java b/java/com/android/dialer/app/calllog/CallLogAdapter.java
index b99cef1..c2c753e 100644
--- a/java/com/android/dialer/app/calllog/CallLogAdapter.java
+++ b/java/com/android/dialer/app/calllog/CallLogAdapter.java
@@ -64,7 +64,6 @@
 import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
 import com.android.dialer.calldetails.CallDetailsEntries;
 import com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry;
-import com.android.dialer.callintent.CallIntentBuilder;
 import com.android.dialer.calllogutils.CallbackActionHelper.CallbackAction;
 import com.android.dialer.calllogutils.PhoneCallDetails;
 import com.android.dialer.common.Assert;
@@ -407,29 +406,8 @@
               }
             }
             expandViewHolderActions(viewHolder);
-
-            if (isDuoCallButtonVisible(viewHolder.videoCallButtonView)) {
-              CallIntentBuilder.increaseLightbringerCallButtonAppearInExpandedCallLogItemCount();
-            }
           }
         }
-
-        private boolean isDuoCallButtonVisible(View videoCallButtonView) {
-          if (videoCallButtonView == null) {
-            return false;
-          }
-          if (videoCallButtonView.getVisibility() != View.VISIBLE) {
-            return false;
-          }
-          IntentProvider intentProvider = (IntentProvider) videoCallButtonView.getTag();
-          if (intentProvider == null) {
-            return false;
-          }
-          return DuoComponent.get(activity)
-              .getDuo()
-              .getIntentType(intentProvider.getIntent(activity))
-              .isPresent();
-        }
       };
 
   @Nullable
diff --git a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
index 5474838..9b7741d 100644
--- a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
+++ b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
@@ -19,7 +19,6 @@
 import android.Manifest.permission;
 import android.annotation.SuppressLint;
 import android.app.Activity;
-import android.content.ActivityNotFoundException;
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.Resources;
@@ -51,7 +50,6 @@
 import android.widget.ImageButton;
 import android.widget.ImageView;
 import android.widget.TextView;
-import android.widget.Toast;
 import com.android.contacts.common.dialog.CallSubjectDialog;
 import com.android.dialer.app.DialtactsActivity;
 import com.android.dialer.app.R;
@@ -96,7 +94,6 @@
 import com.android.dialer.util.CallUtil;
 import com.android.dialer.util.DialerUtils;
 import com.android.dialer.util.UriUtils;
-import com.google.common.base.Optional;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.ref.WeakReference;
@@ -550,7 +547,8 @@
       case CallbackAction.DUO:
         if (showDuoPrimaryButton()) {
           CallIntentBuilder.increaseLightbringerCallButtonAppearInCollapsedCallLogItemCount();
-          primaryActionButtonView.setTag(IntentProvider.getDuoVideoIntentProvider(number));
+          primaryActionButtonView.setTag(
+              IntentProvider.getDuoVideoIntentProvider(number, isNonContactEntry(info)));
         } else {
           primaryActionButtonView.setTag(IntentProvider.getReturnVideoCallIntentProvider(number));
         }
@@ -684,14 +682,17 @@
 
         boolean identifiedSpamCall = isSpamFeatureEnabled && isSpam;
         if (duo.isReachable(context, number)) {
-          videoCallButtonView.setTag(IntentProvider.getDuoVideoIntentProvider(number));
+          videoCallButtonView.setTag(
+              IntentProvider.getDuoVideoIntentProvider(number, isNonContactEntry(info)));
           videoCallButtonView.setVisibility(View.VISIBLE);
+          CallIntentBuilder.increaseLightbringerCallButtonAppearInExpandedCallLogItemCount();
         } else if (duo.isActivated(context) && !identifiedSpamCall) {
           if (ConfigProviderBindings.get(context)
               .getBoolean("enable_call_log_duo_invite_button", false)) {
             inviteVideoButtonView.setTag(IntentProvider.getDuoInviteIntentProvider(number));
             inviteVideoButtonView.setVisibility(View.VISIBLE);
             Logger.get(context).logImpression(DialerImpression.Type.DUO_CALL_LOG_INVITE_SHOWN);
+            CallIntentBuilder.increaseLightbringerCallButtonAppearInExpandedCallLogItemCount();
           }
         } else if (duo.isEnabled(context) && !identifiedSpamCall) {
           if (!duo.isInstalled(context)) {
@@ -701,6 +702,7 @@
               setUpVideoButtonView.setVisibility(View.VISIBLE);
               Logger.get(context)
                   .logImpression(DialerImpression.Type.DUO_CALL_LOG_SET_UP_INSTALL_SHOWN);
+              CallIntentBuilder.increaseLightbringerCallButtonAppearInExpandedCallLogItemCount();
             }
           } else {
             if (ConfigProviderBindings.get(context)
@@ -709,6 +711,7 @@
               setUpVideoButtonView.setVisibility(View.VISIBLE);
               Logger.get(context)
                   .logImpression(DialerImpression.Type.DUO_CALL_LOG_SET_UP_ACTIVATE_SHOWN);
+              CallIntentBuilder.increaseLightbringerCallButtonAppearInExpandedCallLogItemCount();
             }
           }
         }
@@ -782,7 +785,7 @@
 
     callComposeButtonView.setVisibility(isCallComposerCapable ? View.VISIBLE : View.GONE);
 
-    updateBlockReportActions(isVoicemailNumber);
+    updateBlockReportActions(canPlaceCallToNumber, isVoicemailNumber);
   }
 
   private boolean isFullyUndialableVoicemail() {
@@ -1024,20 +1027,13 @@
     if (intentProvider == null) {
       return;
     }
-
+    intentProvider.logInteraction(context);
     final Intent intent = intentProvider.getIntent(context);
     // See IntentProvider.getCallDetailIntentProvider() for why this may be null.
     if (intent == null) {
       return;
     }
-
-    // We check to see if we are starting a Duo intent. The reason is Duo
-    // intents need to be started using startActivityForResult instead of the usual startActivity
-    Optional<Duo.IntentType> duoIntentType =
-        DuoComponent.get(context).getDuo().getIntentType(intent);
-    if (duoIntentType.isPresent()) {
-      startDuoActivity(intent, duoIntentType.get());
-    } else if (OldCallDetailsActivity.isLaunchIntent(intent)) {
+    if (OldCallDetailsActivity.isLaunchIntent(intent)) {
       PerformanceReport.recordClick(UiAction.Type.OPEN_CALL_DETAIL);
       ((Activity) context)
           .startActivityForResult(intent, ActivityRequestCodes.DIALTACTS_CALL_DETAILS);
@@ -1046,9 +1042,6 @@
           && intent.getIntExtra(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, -1)
               == VideoProfile.STATE_BIDIRECTIONAL) {
         Logger.get(context).logImpression(DialerImpression.Type.IMS_VIDEO_REQUESTED_FROM_CALL_LOG);
-      } else if (intent.filterEquals(
-          DuoComponent.get(context).getDuo().getInstallDuoIntent().orNull())) {
-        Logger.get(context).logImpression(DialerImpression.Type.DUO_CALL_LOG_SET_UP_INSTALL);
       }
 
       DialerUtils.startActivityWithErrorToast(context, intent);
@@ -1062,32 +1055,6 @@
     return false;
   }
 
-  private void startDuoActivity(Intent intent, Duo.IntentType intentType) {
-    switch (intentType) {
-      case CALL:
-        Logger.get(context)
-            .logImpression(DialerImpression.Type.LIGHTBRINGER_VIDEO_REQUESTED_FROM_CALL_LOG);
-        if (isNonContactEntry(info)) {
-          Logger.get(context)
-              .logImpression(
-                  DialerImpression.Type.LIGHTBRINGER_NON_CONTACT_VIDEO_REQUESTED_FROM_CALL_LOG);
-        }
-        break;
-      case INVITE:
-        Logger.get(context).logImpression(DialerImpression.Type.DUO_CALL_LOG_INVITE);
-        break;
-      case ACTIVATE:
-        Logger.get(context).logImpression(DialerImpression.Type.DUO_CALL_LOG_SET_UP_ACTIVATE);
-        break;
-    }
-    try {
-      Activity activity = (Activity) context;
-      activity.startActivityForResult(intent, ActivityRequestCodes.DIALTACTS_DUO);
-    } catch (ActivityNotFoundException e) {
-      Toast.makeText(context, R.string.activity_not_available, Toast.LENGTH_SHORT).show();
-    }
-  }
-
   private DialerContact buildContact() {
     DialerContact.Builder contact = DialerContact.newBuilder();
     contact.setPhotoId(info.photoId);
@@ -1172,14 +1139,15 @@
     }
   }
 
-  private void updateBlockReportActions(boolean isVoicemailNumber) {
+  private void updateBlockReportActions(boolean canPlaceCallToNumber, boolean isVoicemailNumber) {
     // Set block/spam actions.
     blockReportView.setVisibility(View.GONE);
     blockView.setVisibility(View.GONE);
     unblockView.setVisibility(View.GONE);
     reportNotSpamView.setVisibility(View.GONE);
     String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso);
-    if (isVoicemailNumber
+    if (!canPlaceCallToNumber
+        || isVoicemailNumber
         || !FilteredNumbersUtil.canBlockNumber(context, e164Number, number)
         || !FilteredNumberCompat.canAttemptBlockOperations(context)) {
       return;
@@ -1260,7 +1228,9 @@
 
     String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso);
     boolean isVoicemailNumber = callLogCache.isVoicemailNumber(accountHandle, number);
-    if (!isVoicemailNumber
+    boolean canPlaceCallToNumber = PhoneNumberHelper.canPlaceCallsTo(number, numberPresentation);
+    if (canPlaceCallToNumber
+        && !isVoicemailNumber
         && FilteredNumbersUtil.canBlockNumber(context, e164Number, number)
         && FilteredNumberCompat.canAttemptBlockOperations(context)) {
       boolean isBlocked = blockId != null;
diff --git a/java/com/android/dialer/app/calllog/IntentProvider.java b/java/com/android/dialer/app/calllog/IntentProvider.java
index 1bc726f..21f3418 100644
--- a/java/com/android/dialer/app/calllog/IntentProvider.java
+++ b/java/com/android/dialer/app/calllog/IntentProvider.java
@@ -32,6 +32,8 @@
 import com.android.dialer.callintent.CallIntentBuilder;
 import com.android.dialer.dialercontact.DialerContact;
 import com.android.dialer.duo.DuoComponent;
+import com.android.dialer.logging.DialerImpression;
+import com.android.dialer.logging.Logger;
 import com.android.dialer.precall.PreCall;
 import com.android.dialer.util.IntentUtil;
 import java.util.ArrayList;
@@ -93,11 +95,26 @@
     };
   }
 
-  public static IntentProvider getDuoVideoIntentProvider(String number) {
+  public static IntentProvider getDuoVideoIntentProvider(String number, boolean isNonContact) {
     return new IntentProvider() {
       @Override
       public Intent getIntent(Context context) {
-        return DuoComponent.get(context).getDuo().getCallIntent(number).orNull();
+        return PreCall.getIntent(
+            context,
+            new CallIntentBuilder(number, CallInitiationType.Type.CALL_LOG)
+                .setIsDuoCall(true)
+                .setIsVideoCall(true));
+      }
+
+      @Override
+      public void logInteraction(Context context) {
+        Logger.get(context)
+            .logImpression(DialerImpression.Type.LIGHTBRINGER_VIDEO_REQUESTED_FROM_CALL_LOG);
+        if (isNonContact) {
+          Logger.get(context)
+              .logImpression(
+                  DialerImpression.Type.LIGHTBRINGER_NON_CONTACT_VIDEO_REQUESTED_FROM_CALL_LOG);
+        }
       }
     };
   }
@@ -108,6 +125,11 @@
       public Intent getIntent(Context context) {
         return DuoComponent.get(context).getDuo().getInstallDuoIntent().orNull();
       }
+
+      @Override
+      public void logInteraction(Context context) {
+        Logger.get(context).logImpression(DialerImpression.Type.DUO_CALL_LOG_SET_UP_INSTALL);
+      }
     };
   }
 
@@ -117,6 +139,11 @@
       public Intent getIntent(Context context) {
         return DuoComponent.get(context).getDuo().getActivateIntent().orNull();
       }
+
+      @Override
+      public void logInteraction(Context context) {
+        Logger.get(context).logImpression(DialerImpression.Type.DUO_CALL_LOG_SET_UP_ACTIVATE);
+      }
     };
   }
 
@@ -126,6 +153,11 @@
       public Intent getIntent(Context context) {
         return DuoComponent.get(context).getDuo().getInviteIntent(number).orNull();
       }
+
+      @Override
+      public void logInteraction(Context context) {
+        Logger.get(context).logImpression(DialerImpression.Type.DUO_CALL_LOG_INVITE);
+      }
     };
   }
 
@@ -244,4 +276,6 @@
   }
 
   public abstract Intent getIntent(Context context);
+
+  public void logInteraction(Context context) {}
 }
diff --git a/java/com/android/dialer/calldetails/CallDetailsActivityCommon.java b/java/com/android/dialer/calldetails/CallDetailsActivityCommon.java
index 80a4191..86a2676 100644
--- a/java/com/android/dialer/calldetails/CallDetailsActivityCommon.java
+++ b/java/com/android/dialer/calldetails/CallDetailsActivityCommon.java
@@ -19,7 +19,6 @@
 import android.Manifest.permission;
 import android.annotation.SuppressLint;
 import android.app.Activity;
-import android.content.ActivityNotFoundException;
 import android.content.Context;
 import android.content.Intent;
 import android.os.Bundle;
@@ -35,7 +34,6 @@
 import android.support.v7.widget.RecyclerView;
 import android.support.v7.widget.Toolbar;
 import android.view.View;
-import android.widget.Toast;
 import com.android.dialer.assisteddialing.ui.AssistedDialingSettingActivity;
 import com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry;
 import com.android.dialer.callintent.CallInitiationType;
@@ -48,9 +46,6 @@
 import com.android.dialer.common.concurrent.DialerExecutorComponent;
 import com.android.dialer.common.concurrent.UiListener;
 import com.android.dialer.common.database.Selection;
-import com.android.dialer.constants.ActivityRequestCodes;
-import com.android.dialer.duo.Duo;
-import com.android.dialer.duo.DuoComponent;
 import com.android.dialer.enrichedcall.EnrichedCallComponent;
 import com.android.dialer.enrichedcall.EnrichedCallManager;
 import com.android.dialer.enrichedcall.historyquery.proto.HistoryResult;
@@ -63,7 +58,6 @@
 import com.android.dialer.precall.PreCall;
 import com.android.dialer.rtt.RttTranscriptActivity;
 import com.android.dialer.rtt.RttTranscriptUtil;
-import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableSet;
 import com.google.i18n.phonenumbers.NumberParseException;
@@ -327,19 +321,11 @@
     public void placeDuoVideoCall(String phoneNumber) {
       Logger.get(getActivity())
           .logImpression(DialerImpression.Type.CALL_DETAILS_LIGHTBRINGER_CALL_BACK);
-      Duo duo = DuoComponent.get(getActivity()).getDuo();
-      Optional<Intent> intentOptional = duo.getCallIntent(phoneNumber);
-      if (!duo.isReachable(getActivity(), phoneNumber) || !intentOptional.isPresent()) {
-        placeImsVideoCall(phoneNumber);
-        return;
-      }
-
-      try {
-        getActivity()
-            .startActivityForResult(intentOptional.get(), ActivityRequestCodes.DIALTACTS_DUO);
-      } catch (ActivityNotFoundException e) {
-        Toast.makeText(getActivity(), R.string.activity_not_available, Toast.LENGTH_SHORT).show();
-      }
+      PreCall.start(
+          getActivity(),
+          new CallIntentBuilder(phoneNumber, CallInitiationType.Type.CALL_DETAILS)
+              .setIsDuoCall(true)
+              .setIsVideoCall(true));
     }
 
     @Override
diff --git a/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java b/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java
index cd1752d..44b5a43 100644
--- a/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java
+++ b/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java
@@ -190,7 +190,13 @@
         .loadQuickContactBadge(contactPhoto, headerInfo.getPhotoInfo());
 
     nameView.setText(headerInfo.getPrimaryText());
-    numberView.setText(headerInfo.getSecondaryText());
+    if (!headerInfo.getSecondaryText().isEmpty()) {
+      numberView.setVisibility(View.VISIBLE);
+      numberView.setText(headerInfo.getSecondaryText());
+    } else {
+      numberView.setVisibility(View.GONE);
+      numberView.setText(null);
+    }
 
     setCallbackAction(callbackAction);
   }
diff --git a/java/com/android/dialer/callintent/CallIntent.java b/java/com/android/dialer/callintent/CallIntent.java
new file mode 100644
index 0000000..ba61d56
--- /dev/null
+++ b/java/com/android/dialer/callintent/CallIntent.java
@@ -0,0 +1,314 @@
+/*
+ * 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.callintent;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
+import android.text.TextUtils;
+import com.android.dialer.common.Assert;
+import com.android.dialer.performancereport.PerformanceReport;
+import com.android.dialer.util.CallUtil;
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableMap;
+import com.google.protobuf.InvalidProtocolBufferException;
+
+/** Creates an intent to start a new outgoing call. */
+@AutoValue
+public abstract class CallIntent implements Parcelable {
+  private static int lightbringerButtonAppearInExpandedCallLogItemCount = 0;
+  private static int lightbringerButtonAppearInCollapsedCallLogItemCount = 0;
+  private static int lightbringerButtonAppearInSearchCount = 0;
+
+  abstract Uri number();
+
+  abstract CallSpecificAppData callSpecificAppData();
+
+  @Nullable
+  abstract PhoneAccountHandle phoneAccountHandle();
+
+  abstract boolean isVideoCall();
+
+  @Nullable
+  abstract String callSubject();
+
+  abstract boolean allowAssistedDial();
+
+  abstract ImmutableMap<String, String> stringInCallUiIntentExtras();
+
+  abstract ImmutableMap<String, Long> longInCallUiIntentExtras();
+
+  abstract ImmutableMap<String, String> stringPlaceCallExtras();
+
+  abstract ImmutableMap<String, Long> longPlaceCallExtras();
+
+  public static Builder builder() {
+    return new AutoValue_CallIntent.Builder().setIsVideoCall(false).setAllowAssistedDial(false);
+  }
+
+  public abstract Builder toBuilder();
+
+  /** Builder class for CallIntent info. */
+  @AutoValue.Builder
+  public abstract static class Builder {
+    public Builder setTelNumber(String number) {
+      return setNumber(CallUtil.getCallUri(Assert.isNotNull(number)));
+    }
+
+    public Builder setVoicemailNumber(@Nullable PhoneAccountHandle phoneAccountHandle) {
+      return setNumber(Uri.fromParts(PhoneAccount.SCHEME_VOICEMAIL, "", null))
+          .setPhoneAccountHandle(phoneAccountHandle);
+    }
+
+    public abstract Builder setNumber(@NonNull Uri number);
+
+    public Builder setCallInitiationType(CallInitiationType.Type callInitiationType) {
+      return setCallSpecificAppData(
+          CallSpecificAppData.newBuilder().setCallInitiationType(callInitiationType).build());
+    }
+
+    abstract CallSpecificAppData callSpecificAppData();
+
+    public abstract Builder setCallSpecificAppData(
+        @NonNull CallSpecificAppData callSpecificAppData);
+
+    public abstract Builder setPhoneAccountHandle(PhoneAccountHandle phoneAccountHandle);
+
+    public abstract Builder setIsVideoCall(boolean isVideoCall);
+
+    public abstract Builder setCallSubject(String callSubject);
+
+    public abstract Builder setAllowAssistedDial(boolean allowAssistedDial);
+
+    abstract ImmutableMap.Builder<String, String> stringInCallUiIntentExtrasBuilder();
+
+    abstract ImmutableMap.Builder<String, Long> longInCallUiIntentExtrasBuilder();
+
+    public Builder addInCallUiIntentExtra(String key, String value) {
+      stringInCallUiIntentExtrasBuilder().put(key, value);
+      return this;
+    }
+
+    public Builder addInCallUiIntentExtra(String key, Long value) {
+      longInCallUiIntentExtrasBuilder().put(key, value);
+      return this;
+    }
+
+    abstract ImmutableMap.Builder<String, String> stringPlaceCallExtrasBuilder();
+
+    abstract ImmutableMap.Builder<String, Long> longPlaceCallExtrasBuilder();
+
+    public Builder addPlaceCallExtra(String key, String value) {
+      stringPlaceCallExtrasBuilder().put(key, value);
+      return this;
+    }
+
+    public Builder addPlaceCallExtra(String key, Long value) {
+      longPlaceCallExtrasBuilder().put(key, value);
+      return this;
+    }
+
+    abstract CallIntent autoBuild();
+
+    public Intent build() {
+      CallSpecificAppData.Builder builder =
+          CallSpecificAppData.newBuilder(callSpecificAppData())
+              .setLightbringerButtonAppearInExpandedCallLogItemCount(
+                  lightbringerButtonAppearInExpandedCallLogItemCount)
+              .setLightbringerButtonAppearInCollapsedCallLogItemCount(
+                  lightbringerButtonAppearInCollapsedCallLogItemCount)
+              .setLightbringerButtonAppearInSearchCount(lightbringerButtonAppearInSearchCount);
+      lightbringerButtonAppearInExpandedCallLogItemCount = 0;
+      lightbringerButtonAppearInCollapsedCallLogItemCount = 0;
+      lightbringerButtonAppearInSearchCount = 0;
+
+      if (PerformanceReport.isRecording()) {
+        builder
+            .setTimeSinceAppLaunch(PerformanceReport.getTimeSinceAppLaunch())
+            .setTimeSinceFirstClick(PerformanceReport.getTimeSinceFirstClick())
+            .addAllUiActionsSinceAppLaunch(PerformanceReport.getActions())
+            .addAllUiActionTimestampsSinceAppLaunch(PerformanceReport.getActionTimestamps())
+            .setStartingTabIndex(PerformanceReport.getStartingTabIndex())
+            .build();
+        PerformanceReport.stopRecording();
+      }
+
+      setCallSpecificAppData(builder.build());
+
+      // Validate CallIntent.
+      CallIntent callIntent = autoBuild();
+      Assert.isNotNull(callIntent.number());
+      Assert.isNotNull(callIntent.callSpecificAppData());
+      Assert.checkArgument(
+          callIntent.callSpecificAppData().getCallInitiationType()
+              != CallInitiationType.Type.UNKNOWN_INITIATION);
+
+      return autoBuild().newIntent();
+    }
+  }
+
+  // Creates the intent which can start a call
+  private Intent newIntent() {
+    Intent intent = new Intent(Intent.ACTION_CALL, number());
+
+    intent.putExtra(
+        TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+        isVideoCall() ? VideoProfile.STATE_BIDIRECTIONAL : VideoProfile.STATE_AUDIO_ONLY);
+
+    Bundle inCallUiIntentExtras = createInCallUiIntentExtras();
+    inCallUiIntentExtras.putLong(
+        Constants.EXTRA_CALL_CREATED_TIME_MILLIS, SystemClock.elapsedRealtime());
+
+    intent.putExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, inCallUiIntentExtras);
+
+    if (phoneAccountHandle() != null) {
+      intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle());
+    }
+
+    if (!TextUtils.isEmpty(callSubject())) {
+      intent.putExtra(TelecomManager.EXTRA_CALL_SUBJECT, callSubject());
+    }
+
+    intent.putExtras(createPlaceCallExtras());
+
+    return intent;
+  }
+
+  @SuppressWarnings("AndroidApiChecker") // Use of Java 8 APIs.
+  private Bundle createInCallUiIntentExtras() {
+    Bundle bundle = new Bundle();
+    stringInCallUiIntentExtras().forEach(bundle::putString);
+    longInCallUiIntentExtras().forEach(bundle::putLong);
+    CallIntentParser.putCallSpecificAppData(bundle, callSpecificAppData());
+    return bundle;
+  }
+
+  @SuppressWarnings("AndroidApiChecker") // Use of Java 8 APIs.
+  private Bundle createPlaceCallExtras() {
+    Bundle bundle = new Bundle();
+    stringPlaceCallExtras().forEach(bundle::putString);
+    longPlaceCallExtras().forEach(bundle::putLong);
+    CallIntentParser.putCallSpecificAppData(bundle, callSpecificAppData());
+    return bundle;
+  }
+
+  public static void increaseLightbringerCallButtonAppearInExpandedCallLogItemCount() {
+    CallIntent.lightbringerButtonAppearInExpandedCallLogItemCount++;
+  }
+
+  public static void increaseLightbringerCallButtonAppearInCollapsedCallLogItemCount() {
+    CallIntent.lightbringerButtonAppearInCollapsedCallLogItemCount++;
+  }
+
+  public static void increaseLightbringerCallButtonAppearInSearchCount() {
+    CallIntent.lightbringerButtonAppearInSearchCount++;
+  }
+
+  @VisibleForTesting
+  public static int getLightbringerButtonAppearInExpandedCallLogItemCount() {
+    return lightbringerButtonAppearInExpandedCallLogItemCount;
+  }
+
+  @VisibleForTesting
+  public static int getLightbringerButtonAppearInCollapsedCallLogItemCount() {
+    return lightbringerButtonAppearInCollapsedCallLogItemCount;
+  }
+
+  @VisibleForTesting
+  public static int getLightbringerButtonAppearInSearchCount() {
+    return lightbringerButtonAppearInSearchCount;
+  }
+
+  @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+  public static void clearLightbringerCounts() {
+    lightbringerButtonAppearInCollapsedCallLogItemCount = 0;
+    lightbringerButtonAppearInExpandedCallLogItemCount = 0;
+    lightbringerButtonAppearInSearchCount = 0;
+  }
+
+  @Override
+  public int describeContents() {
+    return 0;
+  }
+
+  @SuppressWarnings("AndroidApiChecker") // Use of Java 8 APIs.
+  @Override
+  public void writeToParcel(Parcel dest, int flags) {
+    dest.writeParcelable(number(), flags);
+    dest.writeByteArray(callSpecificAppData().toByteArray());
+    dest.writeParcelable(phoneAccountHandle(), flags);
+    dest.writeInt(isVideoCall() ? 1 : 0);
+    dest.writeString(callSubject());
+    dest.writeInt(allowAssistedDial() ? 1 : 0);
+    Bundle stringInCallUiIntentExtrasBundle = new Bundle();
+    stringInCallUiIntentExtras().forEach(stringInCallUiIntentExtrasBundle::putString);
+    dest.writeBundle(stringInCallUiIntentExtrasBundle);
+    Bundle longInCallUiIntentExtrasBundle = new Bundle();
+    longInCallUiIntentExtras().forEach(longInCallUiIntentExtrasBundle::putLong);
+    dest.writeBundle(longInCallUiIntentExtrasBundle);
+  }
+
+  // @TODO(justinmcclain): Investigate deleting the parcelable logic and instead switching
+  // to using an internal proto for serialization.
+  public static final Creator<CallIntent> CREATOR =
+      new Creator<CallIntent>() {
+        @Override
+        public CallIntent createFromParcel(Parcel source) {
+          CallIntent.Builder callIntentBuilder = builder();
+          ClassLoader classLoader = CallIntent.class.getClassLoader();
+          callIntentBuilder.setNumber(source.readParcelable(classLoader));
+          CallSpecificAppData data;
+          try {
+            data = CallSpecificAppData.parseFrom(source.createByteArray());
+          } catch (InvalidProtocolBufferException e) {
+            data = CallSpecificAppData.getDefaultInstance();
+          }
+          callIntentBuilder
+              .setCallSpecificAppData(data)
+              .setPhoneAccountHandle(source.readParcelable(classLoader))
+              .setIsVideoCall(source.readInt() != 0)
+              .setCallSubject(source.readString())
+              .setAllowAssistedDial(source.readInt() != 0);
+          Bundle stringInCallUiIntentExtrasBundle = source.readBundle(classLoader);
+          for (String key : stringInCallUiIntentExtrasBundle.keySet()) {
+            callIntentBuilder.addInCallUiIntentExtra(
+                key, stringInCallUiIntentExtrasBundle.getString(key));
+          }
+          Bundle longInCallUiIntentExtrasBundle = source.readBundle(classLoader);
+          for (String key : longInCallUiIntentExtrasBundle.keySet()) {
+            callIntentBuilder.addInCallUiIntentExtra(
+                key, longInCallUiIntentExtrasBundle.getLong(key));
+          }
+          return callIntentBuilder.autoBuild();
+        }
+
+        @Override
+        public CallIntent[] newArray(int size) {
+          return new CallIntent[0];
+        }
+      };
+}
diff --git a/java/com/android/dialer/callintent/CallIntentBuilder.java b/java/com/android/dialer/callintent/CallIntentBuilder.java
index 92efd39..613fdf6 100644
--- a/java/com/android/dialer/callintent/CallIntentBuilder.java
+++ b/java/com/android/dialer/callintent/CallIntentBuilder.java
@@ -43,6 +43,7 @@
   private final CallSpecificAppData callSpecificAppData;
   @Nullable private PhoneAccountHandle phoneAccountHandle;
   private boolean isVideoCall;
+  private boolean isDuoCall;
   private String callSubject;
   private boolean allowAssistedDial;
 
@@ -109,6 +110,7 @@
     callSpecificAppData = data;
     phoneAccountHandle = parcel.readParcelable(classLoader);
     isVideoCall = parcel.readInt() != 0;
+    isDuoCall = parcel.readInt() != 0;
     callSubject = parcel.readString();
     allowAssistedDial = parcel.readInt() != 0;
     inCallUiIntentExtras.putAll(parcel.readBundle(classLoader));
@@ -152,6 +154,15 @@
     return isVideoCall;
   }
 
+  public CallIntentBuilder setIsDuoCall(boolean isDuoCall) {
+    this.isDuoCall = isDuoCall;
+    return this;
+  }
+
+  public boolean isDuoCall() {
+    return isDuoCall;
+  }
+
   /** Default false. Should only be set to true if the number has a lookup URI. */
   public CallIntentBuilder setAllowAssistedDial(boolean allowAssistedDial) {
     this.allowAssistedDial = allowAssistedDial;
@@ -267,6 +278,7 @@
     dest.writeByteArray(callSpecificAppData.toByteArray());
     dest.writeParcelable(phoneAccountHandle, flags);
     dest.writeInt(isVideoCall ? 1 : 0);
+    dest.writeInt(isDuoCall ? 1 : 0);
     dest.writeString(callSubject);
     dest.writeInt(allowAssistedDial ? 1 : 0);
     dest.writeBundle(inCallUiIntentExtras);
diff --git a/java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java b/java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java
index 32c2788..fb3700e 100644
--- a/java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java
+++ b/java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java
@@ -156,8 +156,7 @@
     for (CallLogDataSource dataSource : dataSources.getDataSourcesIncludingSystemCallLog()) {
       ListenableFuture<Boolean> dataSourceDirty = dataSource.isDirty();
       isDirtyFutures.add(dataSourceDirty);
-      String eventName =
-          String.format(Metrics.IS_DIRTY_TEMPLATE, dataSource.getClass().getSimpleName());
+      String eventName = String.format(Metrics.IS_DIRTY_TEMPLATE, dataSource.getLoggingName());
       futureTimer.applyTiming(dataSourceDirty, eventName, LogCatMode.LOG_VALUES);
     }
     // Simultaneously invokes isDirty on all data sources, returning as soon as one returns true.
@@ -242,7 +241,7 @@
   private static String eventNameForFill(CallLogDataSource dataSource, boolean isBuilt) {
     return String.format(
         !isBuilt ? Metrics.INITIAL_FILL_TEMPLATE : Metrics.FILL_TEMPLATE,
-        dataSource.getClass().getSimpleName());
+        dataSource.getLoggingName());
   }
 
   private static String eventNameForOverallFill(boolean isBuilt) {
@@ -255,7 +254,7 @@
         !isBuilt
             ? Metrics.INITIAL_ON_SUCCESSFUL_FILL_TEMPLATE
             : Metrics.ON_SUCCESSFUL_FILL_TEMPLATE,
-        dataSource.getClass().getSimpleName());
+        dataSource.getLoggingName());
   }
 
   private static String eventNameForOverallOnSuccessfulFill(boolean isBuilt) {
diff --git a/java/com/android/dialer/calllog/database/contract/number_attributes.proto b/java/com/android/dialer/calllog/database/contract/number_attributes.proto
index f42974d..2c46d1b 100644
--- a/java/com/android/dialer/calllog/database/contract/number_attributes.proto
+++ b/java/com/android/dialer/calllog/database/contract/number_attributes.proto
@@ -24,7 +24,7 @@
 import "java/com/android/dialer/logging/contact_source.proto";
 
 // Information related to the phone number of the call.
-// Next ID: 14
+// Next ID: 15
 message NumberAttributes {
   // The name (which may be a person's name or business name, but not a number)
   // formatted exactly as it should appear to the user. If the user's locale or
@@ -74,4 +74,7 @@
   // Description of the number's geolocation (e.g., "Mountain View, CA").
   // This string is for display purpose only.
   optional string geolocation = 13;
+
+  // Whether the number is an emergency number.
+  optional bool is_emergency_number = 14;
 }
\ No newline at end of file
diff --git a/java/com/android/dialer/calllog/datasources/CallLogDataSource.java b/java/com/android/dialer/calllog/datasources/CallLogDataSource.java
index f6796c7..75f06d5 100644
--- a/java/com/android/dialer/calllog/datasources/CallLogDataSource.java
+++ b/java/com/android/dialer/calllog/datasources/CallLogDataSource.java
@@ -113,4 +113,11 @@
    */
   @MainThread
   ListenableFuture<Void> clearData();
+
+  /**
+   * The name of this daa source for logging purposes. This is generally the same as the class name
+   * (but should not use methods from {@link Class} because the class names are generally obfuscated
+   * by Proguard.
+   */
+  String getLoggingName();
 }
diff --git a/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java b/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java
index 0de9873..66d29a7 100644
--- a/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java
+++ b/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java
@@ -320,6 +320,11 @@
         MoreExecutors.directExecutor());
   }
 
+  @Override
+  public String getLoggingName() {
+    return "PhoneLookupDataSource";
+  }
+
   private static ImmutableSet<DialerPhoneNumber>
       queryDistinctDialerPhoneNumbersFromAnnotatedCallLog(Context appContext) {
     ImmutableSet.Builder<DialerPhoneNumber> numbers = ImmutableSet.builder();
diff --git a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java
index b5067da..a08b50e 100644
--- a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java
+++ b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java
@@ -148,6 +148,11 @@
   }
 
   @Override
+  public String getLoggingName() {
+    return "SystemCallLogDataSource";
+  }
+
+  @Override
   public ListenableFuture<Boolean> isDirty() {
     return backgroundExecutorService.submit(this::isDirtyInternal);
   }
diff --git a/java/com/android/dialer/calllog/datasources/voicemail/VoicemailDataSource.java b/java/com/android/dialer/calllog/datasources/voicemail/VoicemailDataSource.java
index ab9288a..7a23022 100644
--- a/java/com/android/dialer/calllog/datasources/voicemail/VoicemailDataSource.java
+++ b/java/com/android/dialer/calllog/datasources/voicemail/VoicemailDataSource.java
@@ -121,4 +121,9 @@
   public ListenableFuture<Void> clearData() {
     return Futures.immediateFuture(null);
   }
+
+  @Override
+  public String getLoggingName() {
+    return "VoicemailDataSource";
+  }
 }
diff --git a/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java b/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java
index f373a7c..7fd8132 100644
--- a/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java
+++ b/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java
@@ -15,6 +15,7 @@
  */
 package com.android.dialer.calllog.ui;
 
+import android.app.Activity;
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.database.Cursor;
@@ -72,7 +73,7 @@
   }
 
   private final Clock clock;
-  private final Context context;
+  private final Activity activity;
   private final RealtimeRowProcessor realtimeRowProcessor;
   private final PopCounts popCounts = new PopCounts();
   private final SharedPreferences sharedPref;
@@ -93,12 +94,12 @@
   /** Position of the "Older" header. Null when it should not be displayed. */
   @Nullable private Integer olderHeaderPosition;
 
-  NewCallLogAdapter(Context context, Cursor cursor, Clock clock) {
-    this.context = context;
+  NewCallLogAdapter(Activity activity, Cursor cursor, Clock clock) {
+    this.activity = activity;
     this.cursor = cursor;
     this.clock = clock;
-    this.realtimeRowProcessor = CallLogUiComponent.get(context).realtimeRowProcessor();
-    this.sharedPref = StorageComponent.get(context).unencryptedSharedPrefs();
+    this.realtimeRowProcessor = CallLogUiComponent.get(activity).realtimeRowProcessor();
+    this.sharedPref = StorageComponent.get(activity).unencryptedSharedPrefs();
     this.onScrollListenerForRecordingDuoDisclosureFirstViewTime =
         new OnScrollListenerForRecordingDuoDisclosureFirstViewTime(sharedPref, clock);
 
@@ -175,8 +176,8 @@
     // Don't show the Duo disclosure card if
     // (1) Duo integration is not enabled on the device, or
     // (2) Duo is not activated.
-    Duo duo = DuoComponent.get(context).getDuo();
-    if (!duo.isEnabled(context) || !duo.isActivated(context)) {
+    Duo duo = DuoComponent.get(activity).getDuo();
+    if (!duo.isEnabled(activity) || !duo.isActivated(activity)) {
       return false;
     }
 
@@ -218,7 +219,7 @@
     switch (viewType) {
       case RowType.DUO_DISCLOSURE_CARD:
         return new DuoDisclosureCardViewHolder(
-            LayoutInflater.from(context)
+            LayoutInflater.from(activity)
                 .inflate(
                     R.layout.new_call_log_duo_disclosure_card,
                     viewGroup,
@@ -227,11 +228,12 @@
       case RowType.HEADER_YESTERDAY:
       case RowType.HEADER_OLDER:
         return new HeaderViewHolder(
-            LayoutInflater.from(context)
+            LayoutInflater.from(activity)
                 .inflate(R.layout.new_call_log_header, viewGroup, /* attachToRoot = */ false));
       case RowType.CALL_LOG_ENTRY:
         return new NewCallLogViewHolder(
-            LayoutInflater.from(context)
+            activity,
+            LayoutInflater.from(activity)
                 .inflate(R.layout.new_call_log_entry, viewGroup, /* attachToRoot = */ false),
             clock,
             realtimeRowProcessor,
@@ -249,7 +251,7 @@
         ((DuoDisclosureCardViewHolder) viewHolder)
             .setDismissListener(
                 unused -> {
-                  StorageComponent.get(context)
+                  StorageComponent.get(activity)
                       .unencryptedSharedPrefs()
                       .edit()
                       .putBoolean(SHARED_PREF_KEY_DUO_DISCLOSURE_DISMISSED, true)
diff --git a/java/com/android/dialer/calllog/ui/NewCallLogFragment.java b/java/com/android/dialer/calllog/ui/NewCallLogFragment.java
index 0f1c251..bc57507 100644
--- a/java/com/android/dialer/calllog/ui/NewCallLogFragment.java
+++ b/java/com/android/dialer/calllog/ui/NewCallLogFragment.java
@@ -15,6 +15,7 @@
  */
 package com.android.dialer.calllog.ui;
 
+import android.app.Activity;
 import android.database.Cursor;
 import android.os.Bundle;
 import android.support.annotation.Nullable;
@@ -30,6 +31,7 @@
 import android.view.ViewGroup;
 import com.android.dialer.calllog.CallLogComponent;
 import com.android.dialer.calllog.RefreshAnnotatedCallLogReceiver;
+import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.common.concurrent.DefaultFutureCallback;
 import com.android.dialer.common.concurrent.ThreadUtil;
@@ -229,8 +231,11 @@
     // TODO(zachh): Handle empty cursor by showing empty view.
     if (recyclerView.getAdapter() == null) {
       recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
+      // Note: It's not clear if this callback can be invoked when there's no associated activity,
+      // but if crashes are observed here it may be possible to use getContext() instead.
+      Activity activity = Assert.isNotNull(getActivity());
       recyclerView.setAdapter(
-          new NewCallLogAdapter(getContext(), newCursor, System::currentTimeMillis));
+          new NewCallLogAdapter(activity, newCursor, System::currentTimeMillis));
     } else {
       ((NewCallLogAdapter) recyclerView.getAdapter()).updateCursor(newCursor);
     }
diff --git a/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java b/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java
index c02d80e..5f3cd96 100644
--- a/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java
+++ b/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java
@@ -72,9 +72,13 @@
   private long currentRowId;
 
   NewCallLogViewHolder(
-      View view, Clock clock, RealtimeRowProcessor realtimeRowProcessor, PopCounts popCounts) {
+      Activity activity,
+      View view,
+      Clock clock,
+      RealtimeRowProcessor realtimeRowProcessor,
+      PopCounts popCounts) {
     super(view);
-    this.activity = (Activity) view.getContext();
+    this.activity = activity;
     contactPhotoView = view.findViewById(R.id.contact_photo_view);
     primaryTextView = view.findViewById(R.id.primary_text);
     callCountTextView = view.findViewById(R.id.call_count);
diff --git a/java/com/android/dialer/calllog/ui/menu/Modules.java b/java/com/android/dialer/calllog/ui/menu/Modules.java
index b06e0fb..cfeca10 100644
--- a/java/com/android/dialer/calllog/ui/menu/Modules.java
+++ b/java/com/android/dialer/calllog/ui/menu/Modules.java
@@ -126,6 +126,7 @@
         .setCanSupportAssistedDialing(canSupportAssistedDialing(row))
         .setCanSupportCarrierVideoCall(row.getNumberAttributes().getCanSupportCarrierVideoCall())
         .setIsBlocked(row.getNumberAttributes().getIsBlocked())
+        .setIsEmergencyNumber(row.getNumberAttributes().getIsEmergencyNumber())
         .setIsSpam(row.getNumberAttributes().getIsSpam())
         .setIsVoicemailCall(row.getIsVoicemailCall())
         .setContactSource(row.getNumberAttributes().getContactSource())
diff --git a/java/com/android/dialer/calllog/ui/res/layout/new_call_log_entry.xml b/java/com/android/dialer/calllog/ui/res/layout/new_call_log_entry.xml
index e3052c0..2fc8e7b 100644
--- a/java/com/android/dialer/calllog/ui/res/layout/new_call_log_entry.xml
+++ b/java/com/android/dialer/calllog/ui/res/layout/new_call_log_entry.xml
@@ -17,6 +17,7 @@
 
 <RelativeLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:minHeight="72dp">
@@ -29,22 +30,34 @@
       android:layout_marginEnd="10dp"
       android:layout_centerVertical="true"/>
 
-  <!-- The frame layout is necessary to avoid clipping the icons and ellipsize the text when the
-       content is too wide to fit.
-   -->
-  <FrameLayout
-      android:id="@+id/primary_row"
+  <LinearLayout
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
+      android:layout_marginTop="14dp"
       android:layout_toEndOf="@+id/contact_photo_view"
-      android:layout_toStartOf="@+id/menu_button">
+      android:layout_toStartOf="@+id/menu_button"
+      android:orientation="vertical">
 
+    <!-- 1st row: primary info -->
     <LinearLayout
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:layout_marginTop="14dp"
         android:orientation="horizontal">
 
+      <!--
+          Important note:
+
+          The following TextView is the only widget that defines a weight in the containing
+          LinearLayout, of which the purpose is to avoid pushing the widgets after it out of the
+          boundary when the text is too long.
+
+          Generally it is more efficient to assign a width/height of 0dp so that the TextView does
+          not have to measure its own size since it will absorb all the remaining space anyway.
+
+          However, as the TextView is part of an entry in the call log's RecyclerView, we must set
+          layout_width to "wrap_content" so that the TextView can adjust its size when recycled for
+          text of different lengths.
+      -->
       <TextView
           android:id="@+id/primary_text"
           style="@style/PrimaryText"
@@ -54,32 +67,29 @@
           android:layout_marginEnd="6dp"
           android:ellipsize="end"
           android:lineSpacingMultiplier="1.5"
-          android:singleLine="true"/>
-
+          android:singleLine="true"
+          tools:ignore="InefficientWeight"/>
 
       <ImageView
           android:id="@+id/hd_icon"
           android:layout_width="wrap_content"
           android:layout_height="18dp"
           android:layout_gravity="center_vertical"
-          android:src="@drawable/quantum_ic_hd_vd_theme_24"
-          />
+          android:src="@drawable/quantum_ic_hd_vd_theme_24"/>
 
       <ImageView
           android:id="@+id/wifi_icon"
           android:layout_width="wrap_content"
           android:layout_height="18dp"
           android:layout_gravity="center_vertical"
-          android:src="@drawable/quantum_ic_signal_wifi_4_bar_vd_theme_24"
-          />
+          android:src="@drawable/quantum_ic_signal_wifi_4_bar_vd_theme_24"/>
 
       <ImageView
           android:id="@+id/assisted_dial_icon"
           android:layout_width="wrap_content"
           android:layout_height="18dp"
           android:layout_gravity="center_vertical"
-          android:src="@drawable/quantum_ic_language_vd_theme_24"
-          />
+          android:src="@drawable/quantum_ic_language_vd_theme_24"/>
 
       <TextView
           android:id="@+id/call_count"
@@ -90,43 +100,40 @@
           android:lineSpacingMultiplier="1.5"/>
 
     </LinearLayout>
-  </FrameLayout>
 
-  <LinearLayout
-      android:id="@+id/secondary_row"
-      android:layout_width="wrap_content"
-      android:layout_height="wrap_content"
-      android:layout_below="@+id/primary_row"
-      android:layout_toEndOf="@+id/contact_photo_view"
-      android:orientation="horizontal">
-
-    <ImageView
-        android:id="@+id/call_type_icon"
+    <!-- 2nd row: secondary info -->
+    <LinearLayout
         android:layout_width="wrap_content"
-        android:layout_height="18dp"
-        android:layout_gravity="center_vertical"
-        />
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
 
+      <ImageView
+          android:id="@+id/call_type_icon"
+          android:layout_width="wrap_content"
+          android:layout_height="18dp"
+          android:layout_gravity="center_vertical"/>
+
+      <TextView
+          android:id="@+id/secondary_text"
+          style="@style/SecondaryText"
+          android:layout_width="wrap_content"
+          android:layout_height="wrap_content"
+          android:ellipsize="end"
+          android:lineSpacingMultiplier="1.4"
+          android:singleLine="true"/>
+
+    </LinearLayout>
+
+    <!-- 3rd row: phone account info -->
     <TextView
-        android:id="@+id/secondary_text"
+        android:id="@+id/phone_account"
         style="@style/SecondaryText"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:ellipsize="end"
-        android:lineSpacingMultiplier="1.4"
         android:singleLine="true"/>
-  </LinearLayout>
 
-  <TextView
-      android:id="@+id/phone_account"
-      style="@style/SecondaryText"
-      android:layout_width="wrap_content"
-      android:layout_height="wrap_content"
-      android:layout_below="@+id/secondary_row"
-      android:layout_toEndOf="@+id/contact_photo_view"
-      android:ellipsize="end"
-      android:singleLine="true"
-      android:visibility="visible"/>
+  </LinearLayout>
 
   <ImageView
       android:id="@+id/menu_button"
diff --git a/java/com/android/dialer/calllogutils/CallLogEntryText.java b/java/com/android/dialer/calllogutils/CallLogEntryText.java
index 1b7bb06..54b1e19 100644
--- a/java/com/android/dialer/calllogutils/CallLogEntryText.java
+++ b/java/com/android/dialer/calllogutils/CallLogEntryText.java
@@ -42,28 +42,35 @@
    * following the primary text.)
    */
   public static CharSequence buildPrimaryText(Context context, CoalescedRow row) {
-    // Always prefer the presentation name, like "Restricted".
+    // Calls to emergency services should be shown as "Emergency number".
+    if (row.getNumberAttributes().getIsEmergencyNumber()) {
+      return context.getText(R.string.emergency_number);
+    }
+
+    // Otherwise, follow the following order of preferences.
+    // 1st preference: the presentation name, like "Restricted".
     Optional<String> presentationName =
         PhoneNumberDisplayUtil.getNameForPresentation(context, row.getNumberPresentation());
     if (presentationName.isPresent()) {
       return presentationName.get();
     }
 
+    // 2nd preference: the voicemail tag if the call is one made to a voicemail box.
     if (row.getIsVoicemailCall() && !TextUtils.isEmpty(row.getVoicemailCallTag())) {
       return row.getVoicemailCallTag();
     }
 
-    // Otherwise prefer the name.
+    // 3rd preference: the name associated with the number.
     if (!TextUtils.isEmpty(row.getNumberAttributes().getName())) {
       return row.getNumberAttributes().getName();
     }
 
-    // Otherwise prefer the formatted number.
+    // 4th preference: the formatted number.
     if (!TextUtils.isEmpty(row.getFormattedNumber())) {
       return row.getFormattedNumber();
     }
 
-    // If there's no formatted number, just return "Unknown".
+    // Last resort: show "Unknown".
     return context.getText(R.string.new_call_log_unknown);
   }
 
@@ -73,6 +80,7 @@
    * <p>Rules:
    *
    * <ul>
+   *   <li>For emergency numbers: Date
    *   <li>For numbers that are not spam or blocked: $Label(, Duo video|Carrier video)?|$Location •
    *       Date
    *   <li>For blocked non-spam numbers: Blocked • $Label(, Duo video|Carrier video)?|$Location •
@@ -100,6 +108,12 @@
    */
   public static CharSequence buildSecondaryTextForEntries(
       Context context, Clock clock, CoalescedRow row) {
+    // For emergency numbers, the secondary text should contain only the timestamp.
+    if (row.getNumberAttributes().getIsEmergencyNumber()) {
+      return CallLogDates.newCallLogTimestampLabel(
+          context, clock.currentTimeMillis(), row.getTimestamp());
+    }
+
     List<CharSequence> components = new ArrayList<>();
 
     if (row.getNumberAttributes().getIsBlocked()) {
@@ -127,6 +141,8 @@
   public static CharSequence buildSecondaryTextForBottomSheet(Context context, CoalescedRow row) {
     /*
      * Rules:
+     *   For emergency numbers:
+     *     Number
      *   For numbers that are not spam or blocked:
      *     $Label(, Duo video|Carrier video)?|$Location [• NumberIfNoName]?
      *   For blocked non-spam numbers:
@@ -149,6 +165,14 @@
      *   Mobile • 555-1234
      *   Brooklyn, NJ
      */
+
+    // For emergency numbers, the secondary text should contain only the number.
+    if (row.getNumberAttributes().getIsEmergencyNumber()) {
+      return !row.getFormattedNumber().isEmpty()
+          ? row.getFormattedNumber()
+          : row.getNumber().getNormalizedNumber();
+    }
+
     List<CharSequence> components = new ArrayList<>();
 
     if (row.getNumberAttributes().getIsBlocked()) {
diff --git a/java/com/android/dialer/calllogutils/CallLogRowActions.java b/java/com/android/dialer/calllogutils/CallLogRowActions.java
index d23a15f..65f3c5f 100644
--- a/java/com/android/dialer/calllogutils/CallLogRowActions.java
+++ b/java/com/android/dialer/calllogutils/CallLogRowActions.java
@@ -20,6 +20,7 @@
 import com.android.dialer.callintent.CallInitiationType;
 import com.android.dialer.callintent.CallIntentBuilder;
 import com.android.dialer.calllog.model.CoalescedRow;
+import com.android.dialer.duo.DuoComponent;
 import com.android.dialer.precall.PreCall;
 
 /** Actions which can be performed on a call log row. */
@@ -37,6 +38,10 @@
         activity,
         new CallIntentBuilder(
                 row.getNumber().getNormalizedNumber(), CallInitiationType.Type.CALL_LOG)
-            .setIsVideoCall((row.getFeatures() & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO));
+            .setIsVideoCall((row.getFeatures() & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO)
+            .setIsDuoCall(
+                DuoComponent.get(activity)
+                    .getDuo()
+                    .isDuoAccount(row.getPhoneAccountComponentName())));
   }
 }
diff --git a/java/com/android/dialer/calllogutils/NumberAttributesConverter.java b/java/com/android/dialer/calllogutils/NumberAttributesConverter.java
index 9f07fda..8081c4b 100644
--- a/java/com/android/dialer/calllogutils/NumberAttributesConverter.java
+++ b/java/com/android/dialer/calllogutils/NumberAttributesConverter.java
@@ -57,6 +57,7 @@
         .setIsCp2InfoIncomplete(phoneLookupInfoConsolidator.isDefaultCp2InfoIncomplete())
         .setContactSource(phoneLookupInfoConsolidator.getContactSource())
         .setCanSupportCarrierVideoCall(phoneLookupInfoConsolidator.canSupportCarrierVideoCall())
-        .setGeolocation(phoneLookupInfoConsolidator.getGeolocation());
+        .setGeolocation(phoneLookupInfoConsolidator.getGeolocation())
+        .setIsEmergencyNumber(phoneLookupInfoConsolidator.isEmergencyNumber());
   }
 }
diff --git a/java/com/android/dialer/common/concurrent/DialerExecutorComponent.java b/java/com/android/dialer/common/concurrent/DialerExecutorComponent.java
index f4552b2..6a22174 100644
--- a/java/com/android/dialer/common/concurrent/DialerExecutorComponent.java
+++ b/java/com/android/dialer/common/concurrent/DialerExecutorComponent.java
@@ -23,6 +23,7 @@
 import com.android.dialer.common.concurrent.Annotations.NonUiParallel;
 import com.android.dialer.common.concurrent.Annotations.Ui;
 import com.android.dialer.inject.HasRootComponent;
+import com.android.dialer.inject.IncludeInDialerRoot;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import dagger.Subcomponent;
 import java.util.concurrent.ExecutorService;
@@ -66,6 +67,7 @@
   }
 
   /** Used to refer to the root application component. */
+  @IncludeInDialerRoot
   public interface HasComponent {
     DialerExecutorComponent dialerExecutorComponent();
   }
diff --git a/java/com/android/dialer/configprovider/ConfigProviderComponent.java b/java/com/android/dialer/configprovider/ConfigProviderComponent.java
index 10d52e7..e974e30 100644
--- a/java/com/android/dialer/configprovider/ConfigProviderComponent.java
+++ b/java/com/android/dialer/configprovider/ConfigProviderComponent.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.support.annotation.NonNull;
 import com.android.dialer.inject.HasRootComponent;
+import com.android.dialer.inject.IncludeInDialerRoot;
 import dagger.Subcomponent;
 
 /** Dagger component to provide a {@link ConfigProvider}. */
@@ -36,6 +37,7 @@
   }
 
   /** Used to refer to the root application component. */
+  @IncludeInDialerRoot
   public interface HasComponent {
     ConfigProviderComponent configProviderComponent();
   }
diff --git a/java/com/android/dialer/configprovider/SharedPrefConfigProviderModule.java b/java/com/android/dialer/configprovider/SharedPrefConfigProviderModule.java
index 4af8bfe..81bed19 100644
--- a/java/com/android/dialer/configprovider/SharedPrefConfigProviderModule.java
+++ b/java/com/android/dialer/configprovider/SharedPrefConfigProviderModule.java
@@ -16,12 +16,15 @@
 
 package com.android.dialer.configprovider;
 
+import com.android.dialer.inject.DialerVariant;
+import com.android.dialer.inject.InstallIn;
 import com.android.dialer.storage.StorageModule;
 import dagger.Binds;
 import dagger.Module;
 import javax.inject.Singleton;
 
 /** Dagger module providing {@link ConfigProvider} based on shared preferences. */
+@InstallIn(variants = {DialerVariant.DIALER_TEST})
 @Module(includes = StorageModule.class)
 public abstract class SharedPrefConfigProviderModule {
 
diff --git a/java/com/android/dialer/database/CallLogQueryHandler.java b/java/com/android/dialer/database/CallLogQueryHandler.java
index e974cc4..a18023c 100644
--- a/java/com/android/dialer/database/CallLogQueryHandler.java
+++ b/java/com/android/dialer/database/CallLogQueryHandler.java
@@ -112,6 +112,7 @@
         .appendOmtpVoicemailStatusSelectionClause(context, where, selectionArgs);
 
     if (TelecomUtil.hasReadWriteVoicemailPermissions(context)) {
+      LogUtil.i("CallLogQueryHandler.fetchVoicemailStatus", "fetching voicemail status");
       startQuery(
           QUERY_VOICEMAIL_STATUS_TOKEN,
           null,
@@ -120,6 +121,10 @@
           where.toString(),
           selectionArgs.toArray(new String[selectionArgs.size()]),
           null);
+    } else {
+      LogUtil.i(
+          "CallLogQueryHandler.fetchVoicemailStatus",
+          "fetching voicemail status failed due to permissions");
     }
   }
 
diff --git a/java/com/android/dialer/dialpadview/DialpadFragment.java b/java/com/android/dialer/dialpadview/DialpadFragment.java
index c45c776..d48870d 100644
--- a/java/com/android/dialer/dialpadview/DialpadFragment.java
+++ b/java/com/android/dialer/dialpadview/DialpadFragment.java
@@ -477,7 +477,13 @@
       return;
     }
     digits.setContentDescription(null);
-    digitsHint.setVisibility(View.GONE);
+
+    // TOOD(77908301): Investigate why this is the case
+    // It's not clear why digitsHint would be null when digits is initialized as the time, so adding
+    // a todo to investigate why.
+    if (digitsHint != null) {
+      digitsHint.setVisibility(View.GONE);
+    }
   }
 
   /**
diff --git a/java/com/android/dialer/duo/Duo.java b/java/com/android/dialer/duo/Duo.java
index 06a3db0..85fe9fb 100644
--- a/java/com/android/dialer/duo/Duo.java
+++ b/java/com/android/dialer/duo/Duo.java
@@ -27,6 +27,8 @@
 import android.telecom.PhoneAccountHandle;
 import com.google.auto.value.AutoValue;
 import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.ListenableFuture;
 import java.util.List;
 
 /** Interface for Duo video call integration. */
@@ -61,7 +63,8 @@
 
   /** Starts a task to update the reachability of the parameter numbers asynchronously. */
   @MainThread
-  void updateReachability(@NonNull Context context, @NonNull List<String> numbers);
+  ListenableFuture<ImmutableMap<String, ReachabilityData>> updateReachability(
+      @NonNull Context context, @NonNull List<String> numbers);
 
   /**
    * Clears the current reachability data and starts a task to load the latest reachability data
@@ -95,19 +98,6 @@
    */
   Optional<Intent> getInviteIntent(String number);
 
-  /** Return value of {@link #getIntentType(Intent)} */
-  enum IntentType {
-    /** The intent is returned by {@link #getCallIntent(String)} */
-    CALL,
-    /** The intent is returned by {@link #getActivateIntent()} */
-    ACTIVATE,
-    /** The intent is returned by {@link #getInviteIntent(String)} */
-    INVITE
-  }
-
-  /** Classifies a Duo intent. Absent if the intent is not a Duo intent. */
-  Optional<IntentType> getIntentType(Intent intent);
-
   Optional<Intent> getInstallDuoIntent();
 
   /** Requests upgrading the parameter ongoing call to a Duo video call. */
diff --git a/java/com/android/dialer/duo/DuoComponent.java b/java/com/android/dialer/duo/DuoComponent.java
index 031ee9e..307832a 100644
--- a/java/com/android/dialer/duo/DuoComponent.java
+++ b/java/com/android/dialer/duo/DuoComponent.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.support.annotation.NonNull;
 import com.android.dialer.inject.HasRootComponent;
+import com.android.dialer.inject.IncludeInDialerRoot;
 import dagger.Subcomponent;
 
 /**
@@ -35,6 +36,7 @@
   }
 
   /** Used to refer to the root application component. */
+  @IncludeInDialerRoot
   public interface HasComponent {
     DuoComponent duoComponent();
   }
diff --git a/java/com/android/dialer/duo/PlaceDuoCallNotifier.java b/java/com/android/dialer/duo/PlaceDuoCallNotifier.java
deleted file mode 100644
index 8fde981..0000000
--- a/java/com/android/dialer/duo/PlaceDuoCallNotifier.java
+++ /dev/null
@@ -1,45 +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.dialer.duo;
-
-import android.content.Context;
-import android.content.Intent;
-import android.support.v4.content.LocalBroadcastManager;
-import com.android.dialer.common.LogUtil;
-
-/** Notifies that a Duo video call should be started. */
-public final class PlaceDuoCallNotifier {
-
-  private PlaceDuoCallNotifier() {}
-
-  /**
-   * Broadcasts an intent notifying that a Duo call should be started.
-   *
-   * <p>See {@link PlaceDuoCallReceiver} for how the intent is handled.
-   *
-   * @param phoneNumber The number to start a Duo call. It can be of any format.
-   */
-  public static void notify(Context context, String phoneNumber) {
-    LogUtil.enterBlock("PlaceDuoCallNotifier.notify");
-
-    Intent intent = new Intent();
-    intent.setAction(PlaceDuoCallReceiver.ACTION_START_DUO_CALL);
-    intent.putExtra(PlaceDuoCallReceiver.EXTRA_PHONE_NUMBER, phoneNumber);
-
-    LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
-  }
-}
diff --git a/java/com/android/dialer/duo/PlaceDuoCallReceiver.java b/java/com/android/dialer/duo/PlaceDuoCallReceiver.java
deleted file mode 100644
index f504aef..0000000
--- a/java/com/android/dialer/duo/PlaceDuoCallReceiver.java
+++ /dev/null
@@ -1,77 +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.dialer.duo;
-
-import android.app.Activity;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import com.android.dialer.common.Assert;
-import com.android.dialer.common.LogUtil;
-import com.android.dialer.constants.ActivityRequestCodes;
-
-/** A {@link BroadcastReceiver} that starts a Duo video call. */
-public final class PlaceDuoCallReceiver extends BroadcastReceiver {
-
-  static final String ACTION_START_DUO_CALL = "start_duo_call";
-  static final String EXTRA_PHONE_NUMBER = "phone_number";
-
-  /**
-   * {@link Activity} needed to launch Duo.
-   *
-   * <p>A Duo call can only be placed via {@link Activity#startActivityForResult(Intent, int)}.
-   */
-  private final Activity activity;
-
-  /** Returns an {@link IntentFilter} containing all actions accepted by this broadcast receiver. */
-  public static IntentFilter getIntentFilter() {
-    IntentFilter intentFilter = new IntentFilter();
-    intentFilter.addAction(ACTION_START_DUO_CALL);
-    return intentFilter;
-  }
-
-  public PlaceDuoCallReceiver(Activity activity) {
-    this.activity = activity;
-  }
-
-  @Override
-  public void onReceive(Context context, Intent intent) {
-    LogUtil.enterBlock("PlaceDuoCallReceiver.onReceive");
-
-    String action = intent.getAction();
-
-    switch (Assert.isNotNull(action)) {
-      case ACTION_START_DUO_CALL:
-        startDuoCall(context, intent);
-        break;
-      default:
-        throw new IllegalStateException("Unsupported action: " + action);
-    }
-  }
-
-  private void startDuoCall(Context context, Intent intent) {
-    LogUtil.enterBlock("PlaceDuoCallReceiver.startDuoCall");
-
-    Assert.checkArgument(intent.hasExtra(EXTRA_PHONE_NUMBER));
-    String phoneNumber = intent.getStringExtra(EXTRA_PHONE_NUMBER);
-
-    Duo duo = DuoComponent.get(context).getDuo();
-    activity.startActivityForResult(
-        duo.getCallIntent(phoneNumber).orNull(), ActivityRequestCodes.DIALTACTS_DUO);
-  }
-}
diff --git a/java/com/android/dialer/duo/stub/DuoStub.java b/java/com/android/dialer/duo/stub/DuoStub.java
index b086714..2131d16 100644
--- a/java/com/android/dialer/duo/stub/DuoStub.java
+++ b/java/com/android/dialer/duo/stub/DuoStub.java
@@ -29,6 +29,9 @@
 import com.android.dialer.duo.Duo;
 import com.android.dialer.duo.DuoListener;
 import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 import java.util.List;
 import javax.inject.Inject;
 
@@ -72,10 +75,12 @@
   }
 
   @Override
-  public void updateReachability(@NonNull Context context, @NonNull List<String> numbers) {
+  public ListenableFuture<ImmutableMap<String, ReachabilityData>> updateReachability(
+      @NonNull Context context, @NonNull List<String> numbers) {
     Assert.isMainThread();
     Assert.isNotNull(context);
     Assert.isNotNull(numbers);
+    return Futures.immediateFuture(ImmutableMap.of());
   }
 
   @Override
@@ -115,11 +120,6 @@
   }
 
   @Override
-  public Optional<IntentType> getIntentType(Intent intent) {
-    return Optional.absent();
-  }
-
-  @Override
   public Optional<Intent> getInstallDuoIntent() {
     return null;
   }
diff --git a/java/com/android/dialer/duo/stub/StubDuoModule.java b/java/com/android/dialer/duo/stub/StubDuoModule.java
index 604406a..8966602 100644
--- a/java/com/android/dialer/duo/stub/StubDuoModule.java
+++ b/java/com/android/dialer/duo/stub/StubDuoModule.java
@@ -17,10 +17,13 @@
 package com.android.dialer.duo.stub;
 
 import com.android.dialer.duo.Duo;
+import com.android.dialer.inject.DialerVariant;
+import com.android.dialer.inject.InstallIn;
 import dagger.Binds;
 import dagger.Module;
 import javax.inject.Singleton;
 
+@InstallIn(variants = {DialerVariant.DIALER_TEST})
 @Module
 public abstract class StubDuoModule {
 
diff --git a/java/com/android/dialer/enrichedcall/EnrichedCallComponent.java b/java/com/android/dialer/enrichedcall/EnrichedCallComponent.java
index 2ed2e94..46afd84 100644
--- a/java/com/android/dialer/enrichedcall/EnrichedCallComponent.java
+++ b/java/com/android/dialer/enrichedcall/EnrichedCallComponent.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.support.annotation.NonNull;
 import com.android.dialer.inject.HasRootComponent;
+import com.android.dialer.inject.IncludeInDialerRoot;
 import dagger.Subcomponent;
 
 /** Subcomponent that can be used to access the enriched call implementation. */
@@ -37,6 +38,7 @@
   }
 
   /** Used to refer to the root application component. */
+  @IncludeInDialerRoot
   public interface HasComponent {
     EnrichedCallComponent enrichedCallComponent();
   }
diff --git a/java/com/android/dialer/enrichedcall/stub/StubEnrichedCallModule.java b/java/com/android/dialer/enrichedcall/stub/StubEnrichedCallModule.java
index 93e1579..5a0618d 100644
--- a/java/com/android/dialer/enrichedcall/stub/StubEnrichedCallModule.java
+++ b/java/com/android/dialer/enrichedcall/stub/StubEnrichedCallModule.java
@@ -18,12 +18,15 @@
 
 import com.android.dialer.enrichedcall.EnrichedCallManager;
 import com.android.dialer.enrichedcall.RcsVideoShareFactory;
+import com.android.dialer.inject.DialerVariant;
+import com.android.dialer.inject.InstallIn;
 import com.android.incallui.videotech.empty.EmptyVideoTech;
 import dagger.Module;
 import dagger.Provides;
 import javax.inject.Singleton;
 
 /** Module which binds {@link EnrichedCallManagerStub}. */
+@InstallIn(variants = {DialerVariant.DIALER_TEST})
 @Module
 public class StubEnrichedCallModule {
 
diff --git a/java/com/android/dialer/glidephotomanager/impl/DefaultLookupUriGenerator.java b/java/com/android/dialer/glidephotomanager/impl/DefaultLookupUriGenerator.java
index 6b90e80..2c4acd4 100644
--- a/java/com/android/dialer/glidephotomanager/impl/DefaultLookupUriGenerator.java
+++ b/java/com/android/dialer/glidephotomanager/impl/DefaultLookupUriGenerator.java
@@ -48,7 +48,7 @@
     try {
       lookupJson.put(Contacts.DISPLAY_NAME, photoInfo.getFormattedNumber());
       // DISPLAY_NAME_SOURCE required by contacts, otherwise the URI will not be recognized.
-      lookupJson.put(Contacts.DISPLAY_NAME_SOURCE, DisplayNameSources.STRUCTURED_NAME);
+      lookupJson.put(Contacts.DISPLAY_NAME_SOURCE, DisplayNameSources.PHONE);
       JSONObject contactRows = new JSONObject();
       JSONObject phone = new JSONObject();
       phone.put(CommonDataKinds.Phone.NUMBER, photoInfo.getFormattedNumber());
diff --git a/java/com/android/dialer/historyitemactions/DuoCallModule.java b/java/com/android/dialer/historyitemactions/DuoCallModule.java
deleted file mode 100644
index e6f31e2..0000000
--- a/java/com/android/dialer/historyitemactions/DuoCallModule.java
+++ /dev/null
@@ -1,56 +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.dialer.historyitemactions;
-
-import android.Manifest.permission;
-import android.content.Context;
-import android.support.annotation.RequiresPermission;
-import com.android.dialer.duo.PlaceDuoCallNotifier;
-
-/** {@link HistoryItemActionModule} for making a Duo call. */
-public class DuoCallModule implements HistoryItemActionModule {
-
-  private final Context context;
-  private final String phoneNumber;
-
-  /**
-   * Creates a module for making a Duo call.
-   *
-   * @param phoneNumber The number to start a Duo call. It can be of any format.
-   */
-  public DuoCallModule(Context context, String phoneNumber) {
-    this.context = context;
-    this.phoneNumber = phoneNumber;
-  }
-
-  @Override
-  public int getStringId() {
-    return R.string.video_call;
-  }
-
-  @Override
-  public int getDrawableId() {
-    return R.drawable.quantum_ic_videocam_vd_white_24;
-  }
-
-  @Override
-  @RequiresPermission(permission.READ_PHONE_STATE)
-  public boolean onClick() {
-    PlaceDuoCallNotifier.notify(context, phoneNumber);
-    return true; // Close the bottom sheet.
-  }
-}
diff --git a/java/com/android/dialer/historyitemactions/HistoryItemActionModulesBuilder.java b/java/com/android/dialer/historyitemactions/HistoryItemActionModulesBuilder.java
index 9af08be..e1c6c96 100644
--- a/java/com/android/dialer/historyitemactions/HistoryItemActionModulesBuilder.java
+++ b/java/com/android/dialer/historyitemactions/HistoryItemActionModulesBuilder.java
@@ -118,6 +118,7 @@
    * <p>This method is a no-op if
    *
    * <ul>
+   *   <li>the call is one made to/received from an emergency number,
    *   <li>the call is one made to a voicemail box,
    *   <li>the number is blocked, or
    *   <li>the number is marked as spam.
@@ -138,25 +139,24 @@
    * capability, and Duo is available, add a Duo video call module.
    */
   public HistoryItemActionModulesBuilder addModuleForVideoCall() {
-    if (moduleInfo.getIsVoicemailCall() || moduleInfo.getIsBlocked() || moduleInfo.getIsSpam()) {
+    if (moduleInfo.getIsEmergencyNumber()
+        || 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());
+    CallIntentBuilder callIntentBuilder =
+        new CallIntentBuilder(moduleInfo.getNormalizedNumber(), getCallInitiationType())
+            .setAllowAssistedDial(moduleInfo.getCanSupportAssistedDialing())
+            .setIsVideoCall(true);
 
     // 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);
+      modules.add(IntentModule.newCallModule(context, callIntentBuilder.setIsDuoCall(isDuoCall())));
       return this;
     }
 
@@ -165,9 +165,9 @@
     //
     // The carrier video call module takes precedence over the Duo module.
     if (canPlaceCarrierVideoCall()) {
-      modules.add(carrierVideoCallModule);
+      modules.add(IntentModule.newCallModule(context, callIntentBuilder));
     } else if (canPlaceDuoCall()) {
-      modules.add(duoVideoCallModule);
+      modules.add(IntentModule.newCallModule(context, callIntentBuilder.setIsDuoCall(true)));
     }
     return this;
   }
@@ -178,6 +178,7 @@
    * <p>The method is a no-op if
    *
    * <ul>
+   *   <li>the call is one made to/received from an emergency number,
    *   <li>the call is one made to a voicemail box,
    *   <li>the number is blocked, or
    *   <li>the number is empty.
@@ -186,7 +187,8 @@
   public HistoryItemActionModulesBuilder addModuleForSendingTextMessage() {
     // TODO(zachh): There are other conditions where this module should not be shown
     // (e.g., business numbers).
-    if (moduleInfo.getIsVoicemailCall()
+    if (moduleInfo.getIsEmergencyNumber()
+        || moduleInfo.getIsVoicemailCall()
         || moduleInfo.getIsBlocked()
         || TextUtils.isEmpty(moduleInfo.getNormalizedNumber())) {
       return this;
@@ -217,6 +219,7 @@
    * <p>The method is a no-op if
    *
    * <ul>
+   *   <li>the call is one made to/received from an emergency number,
    *   <li>the call is one made to a voicemail box,
    *   <li>the number is blocked,
    *   <li>the number is marked as spam,
@@ -225,7 +228,8 @@
    * </ul>
    */
   public HistoryItemActionModulesBuilder addModuleForAddingToContacts() {
-    if (moduleInfo.getIsVoicemailCall()
+    if (moduleInfo.getIsEmergencyNumber()
+        || moduleInfo.getIsVoicemailCall()
         || moduleInfo.getIsBlocked()
         || moduleInfo.getIsSpam()
         || isExistingContact()
@@ -253,7 +257,12 @@
   /**
    * 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>The method is a no-op if
+   *
+   * <ul>
+   *   <li>the call is one made to/received from an emergency number, or
+   *   <li>the call is one made to a voicemail box.
+   * </ul>
    *
    * <p>If a number is marked as spam, add two modules:
    *
@@ -267,7 +276,7 @@
    * <p>If a number is not blocked or marked as spam, add the "Block/Report spam" module.
    */
   public HistoryItemActionModulesBuilder addModuleForBlockedOrSpamNumber() {
-    if (moduleInfo.getIsVoicemailCall()) {
+    if (moduleInfo.getIsEmergencyNumber() || moduleInfo.getIsVoicemailCall()) {
       return this;
     }
 
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
index 99071a7..f7022c2 100644
--- a/java/com/android/dialer/historyitemactions/history_item_action_module_info.proto
+++ b/java/com/android/dialer/historyitemactions/history_item_action_module_info.proto
@@ -10,7 +10,7 @@
 import "java/com/android/dialer/logging/contact_source.proto";
 
 // Contains information needed to construct items (modules) in a bottom sheet.
-// Next ID: 16
+// Next ID: 17
 message HistoryItemActionModuleInfo {
   // The dialer-normalized version of a phone number.
   // See DialerPhoneNumber.normalized_number.
@@ -66,4 +66,7 @@
     VOICEMAIL = 2;
   }
   optional Host host = 15;
+
+  // Whether the number is an emergency number.
+  optional bool is_emergency_number = 16;
 }
diff --git a/java/com/android/dialer/historyitemactions/res/layout/contact_layout.xml b/java/com/android/dialer/historyitemactions/res/layout/contact_layout.xml
index 721740f..0790cf4 100644
--- a/java/com/android/dialer/historyitemactions/res/layout/contact_layout.xml
+++ b/java/com/android/dialer/historyitemactions/res/layout/contact_layout.xml
@@ -20,7 +20,8 @@
     android:layout_height="match_parent"
     android:paddingTop="12dp"
     android:paddingBottom="12dp"
-    android:paddingEnd="8dp"
+    android:paddingStart="10dp"
+    android:paddingEnd="10dp"
     android:gravity="center_vertical"
     android:orientation="horizontal"
     android:background="#FFFFFF">
@@ -29,7 +30,6 @@
       android:id="@+id/contact_photo_view"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
-      android:layout_marginStart="10dp"
       android:layout_marginEnd="10dp"
       android:minHeight="@dimen/contact_actions_image_size"/>
 
diff --git a/java/com/android/dialer/main/impl/MainActivity.java b/java/com/android/dialer/main/impl/MainActivity.java
index 3f660f5..2046b04 100644
--- a/java/com/android/dialer/main/impl/MainActivity.java
+++ b/java/com/android/dialer/main/impl/MainActivity.java
@@ -24,7 +24,6 @@
 import com.android.dialer.calllog.config.CallLogConfigComponent;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
-import com.android.dialer.duo.PlaceDuoCallReceiver;
 import com.android.dialer.interactions.PhoneNumberInteraction.DisambigDialogDismissedListener;
 import com.android.dialer.interactions.PhoneNumberInteraction.InteractionErrorCode;
 import com.android.dialer.interactions.PhoneNumberInteraction.InteractionErrorListener;
@@ -48,9 +47,6 @@
    */
   private ShowBlockReportSpamDialogReceiver showBlockReportSpamDialogReceiver;
 
-  /** {@link android.content.BroadcastReceiver} that starts a Duo call. */
-  private PlaceDuoCallReceiver placeDuoCallReceiver;
-
   public static Intent getShowCallLogIntent(Context context) {
     return getShowTabIntent(context, TabIndex.CALL_LOG);
   }
@@ -83,7 +79,6 @@
     activePeer.onActivityCreate(savedInstanceState);
 
     showBlockReportSpamDialogReceiver = new ShowBlockReportSpamDialogReceiver(getFragmentManager());
-    placeDuoCallReceiver = new PlaceDuoCallReceiver(/* activity = */ this);
   }
 
   protected MainActivityPeer getNewPeer() {
@@ -109,8 +104,6 @@
     LocalBroadcastManager.getInstance(this)
         .registerReceiver(
             showBlockReportSpamDialogReceiver, ShowBlockReportSpamDialogReceiver.getIntentFilter());
-    LocalBroadcastManager.getInstance(this)
-        .registerReceiver(placeDuoCallReceiver, PlaceDuoCallReceiver.getIntentFilter());
   }
 
   @Override
@@ -125,7 +118,6 @@
     activePeer.onActivityPause();
 
     LocalBroadcastManager.getInstance(this).unregisterReceiver(showBlockReportSpamDialogReceiver);
-    LocalBroadcastManager.getInstance(this).unregisterReceiver(placeDuoCallReceiver);
   }
 
   @Override
diff --git a/java/com/android/dialer/main/impl/MainSearchController.java b/java/com/android/dialer/main/impl/MainSearchController.java
index b9a6654..72c46cc 100644
--- a/java/com/android/dialer/main/impl/MainSearchController.java
+++ b/java/com/android/dialer/main/impl/MainSearchController.java
@@ -35,7 +35,6 @@
 import com.android.dialer.app.calllog.CallLogActivity;
 import com.android.dialer.app.settings.DialerSettingsActivity;
 import com.android.dialer.callintent.CallInitiationType;
-import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.constants.ActivityRequestCodes;
 import com.android.dialer.dialpadview.DialpadFragment;
@@ -136,7 +135,10 @@
   }
 
   private void showDialpad(boolean animate, boolean fromNewIntent) {
-    Assert.checkArgument(!isDialpadVisible());
+    if (isDialpadVisible()) {
+      LogUtil.e("MainSearchController.showDialpad", "Dialpad is already visible.");
+      return;
+    }
 
     Logger.get(activity).logScreenView(ScreenEvent.Type.MAIN_DIALPAD, activity);
 
@@ -187,14 +189,32 @@
    */
   private void hideDialpad(boolean animate) {
     LogUtil.enterBlock("MainSearchController.hideDialpad");
-    assertDialpadVisible();
+    DialpadFragment dialpadFragment = getDialpadFragment();
+    if (dialpadFragment == null) {
+      LogUtil.e("MainSearchController.hideDialpad", "Dialpad fragment is null.");
+      return;
+    }
+
+    if (!dialpadFragment.isAdded()) {
+      LogUtil.e("MainSearchController.hideDialpad", "Dialpad fragment is not added.");
+      return;
+    }
+
+    if (dialpadFragment.isHidden()) {
+      LogUtil.e("MainSearchController.hideDialpad", "Dialpad fragment is already hidden.");
+      return;
+    }
+
+    if (!dialpadFragment.isDialpadSlideUp()) {
+      LogUtil.e("MainSearchController.hideDialpad", "Dialpad fragment is already slide down.");
+      return;
+    }
 
     fab.show();
     toolbar.slideDown(animate, fragmentContainer);
     toolbar.transferQueryFromDialpad(getDialpadFragment().getQuery());
     activity.setTitle(R.string.main_activity_label);
 
-    DialpadFragment dialpadFragment = getDialpadFragment();
     dialpadFragment.setAnimate(animate);
     dialpadFragment.slideDown(
         animate,
@@ -295,7 +315,22 @@
   /** Calls {@link #hideDialpad(boolean)}, removes the search fragment and clears the dialpad. */
   private void closeSearch(boolean animate) {
     LogUtil.enterBlock("MainSearchController.closeSearch");
-    assertSearchIsVisible();
+    NewSearchFragment searchFragment = getSearchFragment();
+    if (searchFragment == null) {
+      LogUtil.e("MainSearchController.closeSearch", "Search fragment is null.");
+      return;
+    }
+
+    if (!searchFragment.isAdded()) {
+      LogUtil.e("MainSearchController.closeSearch", "Search fragment isn't added.");
+      return;
+    }
+
+    if (searchFragment.isHidden()) {
+      LogUtil.e("MainSearchController.closeSearch", "Search fragment is already hidden.");
+      return;
+    }
+
     if (isDialpadVisible()) {
       hideDialpad(animate);
     } else if (!fab.isShown()) {
@@ -304,7 +339,7 @@
     showBottomNav();
     toolbar.collapse(animate);
     toolbarShadow.setVisibility(View.GONE);
-    activity.getFragmentManager().beginTransaction().hide(getSearchFragment()).commit();
+    activity.getFragmentManager().beginTransaction().hide(searchFragment).commit();
 
     // Clear the dialpad so the phone number isn't persisted between search sessions.
     DialpadFragment dialpadFragment = getDialpadFragment();
@@ -341,26 +376,11 @@
         && fragment.isDialpadSlideUp();
   }
 
-  private void assertDialpadVisible() {
-    DialpadFragment fragment = getDialpadFragment();
-    Assert.checkArgument(fragment != null, "Dialpad Fragment is null");
-    Assert.checkArgument(fragment.isAdded(), "Dialpad Fragment is no added");
-    Assert.checkArgument(!fragment.isHidden(), "Dialpad Fragment is hidden");
-    Assert.checkArgument(fragment.isDialpadSlideUp(), "Dialpad Fragment is slide down");
-  }
-
   private boolean isSearchVisible() {
     NewSearchFragment fragment = getSearchFragment();
     return fragment != null && fragment.isAdded() && !fragment.isHidden();
   }
 
-  private void assertSearchIsVisible() {
-    NewSearchFragment fragment = getSearchFragment();
-    Assert.checkArgument(fragment != null, "Search Fragment is null");
-    Assert.checkArgument(fragment.isAdded(), "Search Fragment is not added");
-    Assert.checkArgument(!fragment.isHidden(), "Search Fragment is hidden.");
-  }
-
   /** Returns true if the search UI is visible. */
   public boolean isInSearch() {
     return isSearchVisible();
diff --git a/java/com/android/dialer/main/impl/OldMainActivityPeer.java b/java/com/android/dialer/main/impl/OldMainActivityPeer.java
index e3d42fa..e426ed2 100644
--- a/java/com/android/dialer/main/impl/OldMainActivityPeer.java
+++ b/java/com/android/dialer/main/impl/OldMainActivityPeer.java
@@ -460,6 +460,7 @@
   @SuppressLint("MissingPermission")
   @Override
   public void onActivityResume() {
+    LogUtil.enterBlock("MainBottomNavBarBottomNavTabListener.onActivityResume");
     callLogFragmentListener.onActivityResume();
     // Start the thread that updates the smart dial database if the activity is recreated with a
     // language change.
@@ -866,6 +867,10 @@
         new ContentObserver(new Handler()) {
           @Override
           public void onChange(boolean selfChange) {
+            LogUtil.i(
+                "MainCallLogFragmentListener",
+                "voicemailStatusObserver.onChange selfChange:%b",
+                selfChange);
             super.onChange(selfChange);
             callLogQueryHandler.fetchVoicemailStatus();
           }
@@ -885,9 +890,10 @@
     }
 
     private void registerVoicemailStatusContentObserver(Context context) {
-
+      LogUtil.enterBlock("MainCallLogFragmentListener.registerVoicemailStatusContentObserver");
       if (PermissionsUtil.hasReadVoicemailPermissions(context)
           && PermissionsUtil.hasAddVoicemailPermissions(context)) {
+        LogUtil.i("MainCallLogFragmentListener.registerVoicemailStatusContentObserver", "register");
         context
             .getContentResolver()
             .registerContentObserver(
@@ -1013,11 +1019,12 @@
     }
 
     public void onActivityResume() {
+      LogUtil.enterBlock("MainCallLogFragmentListener.onActivityResume");
       activityIsAlive = true;
       registerVoicemailStatusContentObserver(context);
-      if (!bottomNavTabListener.newVoicemailFragmentActive()) {
-        callLogQueryHandler.fetchVoicemailStatus();
-      }
+      // TODO(a bug): Don't use callLogQueryHandler
+      callLogQueryHandler.fetchVoicemailStatus();
+
       if (!bottomNavTabListener.newCallLogFragmentActive()) {
         callLogQueryHandler.fetchMissedCallsUnreadCount();
       }
@@ -1234,7 +1241,7 @@
     private static final String CONTACTS_TAG = "contacts";
     private static final String VOICEMAIL_TAG = "voicemail";
 
-    private final AppCompatActivity activity;
+    private final TransactionSafeActivity activity;
     private final FragmentManager fragmentManager;
     private final android.support.v4.app.FragmentManager supportFragmentManager;
     private final FloatingActionButton fab;
@@ -1242,7 +1249,7 @@
     @TabIndex private int selectedTab = -1;
 
     private MainBottomNavBarBottomNavTabListener(
-        AppCompatActivity activity,
+        TransactionSafeActivity activity,
         FragmentManager fragmentManager,
         android.support.v4.app.FragmentManager supportFragmentManager,
         FloatingActionButton fab) {
@@ -1300,7 +1307,7 @@
       android.support.v4.app.Fragment supportFragment =
           supportFragmentManager.findFragmentByTag(CALL_LOG_TAG);
       if (supportFragment != null) {
-        supportFragmentManager.beginTransaction().remove(supportFragment).commit();
+        supportFragmentManager.beginTransaction().remove(supportFragment).commitAllowingStateLoss();
         // If the NewCallLogFragment was showing, immediately show the old call log fragment
         // instead.
         if (selectedTab == TabIndex.CALL_LOG) {
@@ -1317,7 +1324,7 @@
       android.support.v4.app.Fragment supportFragment =
           supportFragmentManager.findFragmentByTag(VOICEMAIL_TAG);
       if (supportFragment != null) {
-        supportFragmentManager.beginTransaction().remove(supportFragment).commit();
+        supportFragmentManager.beginTransaction().remove(supportFragment).commitAllowingStateLoss();
         // If the NewVoicemailFragment was showing, immediately show the old voicemail fragment
         // instead.
         if (selectedTab == TabIndex.VOICEMAIL) {
@@ -1445,7 +1452,9 @@
             "MainBottomNavBarBottomNavTabListener.showFragment", "Not added yet: " + fragment);
         transaction.add(R.id.fragment_container, fragment, tag);
       }
-      transaction.commit();
+      if (activity.isSafeToCommitTransactions()) {
+        transaction.commit();
+      }
 
       // Handle support fragments.
       // TODO(calderwoodra): Handle other new fragments.
@@ -1471,7 +1480,9 @@
             "Not added yet: " + supportFragment);
         supportTransaction.add(R.id.fragment_container, supportFragment, tag);
       }
-      supportTransaction.commit();
+      if (activity.isSafeToCommitTransactions()) {
+        supportTransaction.commit();
+      }
     }
 
     private void showSupportFragment(
diff --git a/java/com/android/dialer/main/impl/toolbar/MainToolbar.java b/java/com/android/dialer/main/impl/toolbar/MainToolbar.java
index 851d7a0..475383b 100644
--- a/java/com/android/dialer/main/impl/toolbar/MainToolbar.java
+++ b/java/com/android/dialer/main/impl/toolbar/MainToolbar.java
@@ -28,6 +28,7 @@
 import android.widget.ImageButton;
 import android.widget.PopupMenu;
 import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
 import com.android.dialer.util.ViewUtil;
 import com.google.common.base.Optional;
 
@@ -41,7 +42,6 @@
   private SearchBarView searchBar;
   private SearchBarListener listener;
   private MainToolbarMenu overflowMenu;
-  private boolean isSlideUp;
   private boolean hasGlobalLayoutListener;
 
   public MainToolbar(Context context, AttributeSet attrs) {
@@ -78,7 +78,6 @@
       return;
     }
 
-    Assert.checkArgument(!isSlideUp);
     if (getHeight() == 0) {
       hasGlobalLayoutListener = true;
       ViewUtil.doOnGlobalLayout(
@@ -89,7 +88,12 @@
           });
       return;
     }
-    isSlideUp = true;
+
+    if (isSlideUp()) {
+      LogUtil.e("MainToolbar.slideDown", "Already slide up.");
+      return;
+    }
+
     animate()
         .translationY(-getHeight())
         .setDuration(animate ? SLIDE_DURATION : 0)
@@ -105,8 +109,10 @@
 
   /** Slides the toolbar down and back onto the screen. */
   public void slideDown(boolean animate, View container) {
-    Assert.checkArgument(isSlideUp);
-    isSlideUp = false;
+    if (getTranslationY() == 0) {
+      LogUtil.e("MainToolbar.slideDown", "Already slide down.");
+      return;
+    }
     animate()
         .translationY(0)
         .setDuration(animate ? SLIDE_DURATION : 0)
@@ -131,7 +137,7 @@
   }
 
   public boolean isSlideUp() {
-    return isSlideUp;
+    return getHeight() != 0 && getTranslationY() == -getHeight();
   }
 
   public boolean isExpanded() {
diff --git a/java/com/android/dialer/metrics/MetricsComponent.java b/java/com/android/dialer/metrics/MetricsComponent.java
index a3570db..c0d957b 100644
--- a/java/com/android/dialer/metrics/MetricsComponent.java
+++ b/java/com/android/dialer/metrics/MetricsComponent.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import com.android.dialer.inject.HasRootComponent;
+import com.android.dialer.inject.IncludeInDialerRoot;
 import dagger.Subcomponent;
 
 /** Component for metrics. */
@@ -37,6 +38,7 @@
   }
 
   /** Used to refer to the root application component. */
+  @IncludeInDialerRoot
   public interface HasComponent {
     MetricsComponent metricsComponent();
   }
diff --git a/java/com/android/dialer/phonelookup/PhoneLookup.java b/java/com/android/dialer/phonelookup/PhoneLookup.java
index 0b9cbf6..1043ee7 100644
--- a/java/com/android/dialer/phonelookup/PhoneLookup.java
+++ b/java/com/android/dialer/phonelookup/PhoneLookup.java
@@ -135,4 +135,11 @@
    * disabled (because for example there was a problem with it).
    */
   ListenableFuture<Void> clearData();
+
+  /**
+   * The name of this lookup for logging purposes. This is generally the same as the class name (but
+   * should not use methods from {@link Class} because the class names are generally obfuscated by
+   * Proguard.
+   */
+  String getLoggingName();
 }
diff --git a/java/com/android/dialer/phonelookup/PhoneLookupModule.java b/java/com/android/dialer/phonelookup/PhoneLookupModule.java
index c6e91c4..16aa8e5 100644
--- a/java/com/android/dialer/phonelookup/PhoneLookupModule.java
+++ b/java/com/android/dialer/phonelookup/PhoneLookupModule.java
@@ -21,6 +21,7 @@
 import com.android.dialer.phonelookup.cnap.CnapPhoneLookup;
 import com.android.dialer.phonelookup.cp2.Cp2DefaultDirectoryPhoneLookup;
 import com.android.dialer.phonelookup.cp2.Cp2ExtendedDirectoryPhoneLookup;
+import com.android.dialer.phonelookup.emergency.EmergencyPhoneLookup;
 import com.android.dialer.phonelookup.spam.SpamPhoneLookup;
 import com.google.common.collect.ImmutableList;
 import dagger.Module;
@@ -37,6 +38,7 @@
       CnapPhoneLookup cnapPhoneLookup,
       Cp2DefaultDirectoryPhoneLookup cp2DefaultDirectoryPhoneLookup,
       Cp2ExtendedDirectoryPhoneLookup cp2ExtendedDirectoryPhoneLookup,
+      EmergencyPhoneLookup emergencyPhoneLookup,
       SystemBlockedNumberPhoneLookup systemBlockedNumberPhoneLookup,
       SpamPhoneLookup spamPhoneLookup) {
     return ImmutableList.of(
@@ -44,6 +46,7 @@
         cnapPhoneLookup,
         cp2DefaultDirectoryPhoneLookup,
         cp2ExtendedDirectoryPhoneLookup,
+        emergencyPhoneLookup,
         systemBlockedNumberPhoneLookup,
         spamPhoneLookup);
   }
diff --git a/java/com/android/dialer/phonelookup/blockednumber/SystemBlockedNumberPhoneLookup.java b/java/com/android/dialer/phonelookup/blockednumber/SystemBlockedNumberPhoneLookup.java
index fe6642e..4192e79 100644
--- a/java/com/android/dialer/phonelookup/blockednumber/SystemBlockedNumberPhoneLookup.java
+++ b/java/com/android/dialer/phonelookup/blockednumber/SystemBlockedNumberPhoneLookup.java
@@ -181,4 +181,9 @@
   public ListenableFuture<Void> clearData() {
     return Futures.immediateFuture(null);
   }
+
+  @Override
+  public String getLoggingName() {
+    return "SystemBlockedNumberPhoneLookup";
+  }
 }
diff --git a/java/com/android/dialer/phonelookup/cequint/CequintPhoneLookup.java b/java/com/android/dialer/phonelookup/cequint/CequintPhoneLookup.java
index 36d0be4..b045d03 100644
--- a/java/com/android/dialer/phonelookup/cequint/CequintPhoneLookup.java
+++ b/java/com/android/dialer/phonelookup/cequint/CequintPhoneLookup.java
@@ -141,6 +141,11 @@
     return Futures.immediateFuture(null);
   }
 
+  @Override
+  public String getLoggingName() {
+    return "CequintPhoneLookup";
+  }
+
   /**
    * Builds a {@link CequintInfo} proto based on the given {@link CequintCallerIdContact} returned
    * by {@link CequintCallerIdManager}.
diff --git a/java/com/android/dialer/phonelookup/cnap/CnapPhoneLookup.java b/java/com/android/dialer/phonelookup/cnap/CnapPhoneLookup.java
index db7c210..1b78a8e 100644
--- a/java/com/android/dialer/phonelookup/cnap/CnapPhoneLookup.java
+++ b/java/com/android/dialer/phonelookup/cnap/CnapPhoneLookup.java
@@ -157,4 +157,9 @@
   public ListenableFuture<Void> clearData() {
     return Futures.immediateFuture(null);
   }
+
+  @Override
+  public String getLoggingName() {
+    return "CnapPhoneLookup";
+  }
 }
diff --git a/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java b/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java
index 1ac13df..8322329 100644
--- a/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java
+++ b/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java
@@ -89,13 +89,12 @@
     for (PhoneLookup<?> phoneLookup : phoneLookups) {
       ListenableFuture<?> lookupFuture = phoneLookup.lookup(appContext, call);
       String eventName =
-          String.format(Metrics.LOOKUP_FOR_CALL_TEMPLATE, phoneLookup.getClass().getSimpleName());
+          String.format(Metrics.LOOKUP_FOR_CALL_TEMPLATE, phoneLookup.getLoggingName());
       futureTimer.applyTiming(lookupFuture, eventName);
       futures.add(lookupFuture);
     }
     ListenableFuture<PhoneLookupInfo> combinedFuture = combineSubMessageFutures(futures);
-    String eventName =
-        String.format(Metrics.LOOKUP_FOR_CALL_TEMPLATE, CompositePhoneLookup.class.getSimpleName());
+    String eventName = String.format(Metrics.LOOKUP_FOR_CALL_TEMPLATE, getLoggingName());
     futureTimer.applyTiming(combinedFuture, eventName);
     return combinedFuture;
   }
@@ -114,14 +113,12 @@
     for (PhoneLookup<?> phoneLookup : phoneLookups) {
       ListenableFuture<?> lookupFuture = phoneLookup.lookup(dialerPhoneNumber);
       String eventName =
-          String.format(Metrics.LOOKUP_FOR_NUMBER_TEMPLATE, phoneLookup.getClass().getSimpleName());
+          String.format(Metrics.LOOKUP_FOR_NUMBER_TEMPLATE, phoneLookup.getLoggingName());
       futureTimer.applyTiming(lookupFuture, eventName);
       futures.add(lookupFuture);
     }
     ListenableFuture<PhoneLookupInfo> combinedFuture = combineSubMessageFutures(futures);
-    String eventName =
-        String.format(
-            Metrics.LOOKUP_FOR_NUMBER_TEMPLATE, CompositePhoneLookup.class.getSimpleName());
+    String eventName = String.format(Metrics.LOOKUP_FOR_NUMBER_TEMPLATE, getLoggingName());
     futureTimer.applyTiming(combinedFuture, eventName);
     return combinedFuture;
   }
@@ -133,6 +130,7 @@
     return Futures.transform(
         Futures.allAsList(subMessageFutures),
         subMessages -> {
+          Preconditions.checkNotNull(subMessages);
           Builder mergedInfo = PhoneLookupInfo.newBuilder();
           for (int i = 0; i < subMessages.size(); i++) {
             PhoneLookup phoneLookup = phoneLookups.get(i);
@@ -152,16 +150,14 @@
     for (PhoneLookup<?> phoneLookup : phoneLookups) {
       ListenableFuture<Boolean> isDirtyFuture = phoneLookup.isDirty(phoneNumbers);
       futures.add(isDirtyFuture);
-      String eventName =
-          String.format(Metrics.IS_DIRTY_TEMPLATE, phoneLookup.getClass().getSimpleName());
+      String eventName = String.format(Metrics.IS_DIRTY_TEMPLATE, phoneLookup.getLoggingName());
       futureTimer.applyTiming(isDirtyFuture, eventName, LogCatMode.LOG_VALUES);
     }
     // Executes all child lookups (possibly in parallel), completing when the first composite lookup
     // which returns "true" completes, and cancels the others.
     ListenableFuture<Boolean> firstMatching =
         DialerFutures.firstMatching(futures, Preconditions::checkNotNull, false /* defaultValue */);
-    String eventName =
-        String.format(Metrics.IS_DIRTY_TEMPLATE, CompositePhoneLookup.class.getSimpleName());
+    String eventName = String.format(Metrics.IS_DIRTY_TEMPLATE, getLoggingName());
     futureTimer.applyTiming(firstMatching, eventName, LogCatMode.LOG_VALUES);
     return firstMatching;
   }
@@ -178,6 +174,7 @@
     return Futures.transformAsync(
         callLogState.isBuilt(),
         isBuilt -> {
+          Preconditions.checkNotNull(isBuilt);
           List<ListenableFuture<ImmutableMap<DialerPhoneNumber, ?>>> futures = new ArrayList<>();
           for (PhoneLookup phoneLookup : phoneLookups) {
             futures.add(buildSubmapAndGetMostRecentInfo(existingInfoMap, phoneLookup, isBuilt));
@@ -186,6 +183,7 @@
               Futures.transform(
                   Futures.allAsList(futures),
                   (allMaps) -> {
+                    Preconditions.checkNotNull(allMaps);
                     ImmutableMap.Builder<DialerPhoneNumber, PhoneLookupInfo> combinedMap =
                         ImmutableMap.builder();
                     for (DialerPhoneNumber dialerPhoneNumber : existingInfoMap.keySet()) {
@@ -206,7 +204,7 @@
                     return combinedMap.build();
                   },
                   lightweightExecutorService);
-          String eventName = getMostRecentInfoEventName(this, isBuilt);
+          String eventName = getMostRecentInfoEventName(getLoggingName(), isBuilt);
           futureTimer.applyTiming(combinedFuture, eventName);
           return combinedFuture;
         },
@@ -224,7 +222,7 @@
                 phoneLookup.getSubMessage(existingInfoMap.get(dialerPhoneNumber)));
     ListenableFuture<ImmutableMap<DialerPhoneNumber, T>> mostRecentInfoFuture =
         phoneLookup.getMostRecentInfo(ImmutableMap.copyOf(submap));
-    String eventName = getMostRecentInfoEventName(phoneLookup, isBuilt);
+    String eventName = getMostRecentInfoEventName(phoneLookup.getLoggingName(), isBuilt);
     futureTimer.applyTiming(mostRecentInfoFuture, eventName);
     return mostRecentInfoFuture;
   }
@@ -234,17 +232,19 @@
     return Futures.transformAsync(
         callLogState.isBuilt(),
         isBuilt -> {
+          Preconditions.checkNotNull(isBuilt);
           List<ListenableFuture<Void>> futures = new ArrayList<>();
           for (PhoneLookup<?> phoneLookup : phoneLookups) {
             ListenableFuture<Void> phoneLookupFuture = phoneLookup.onSuccessfulBulkUpdate();
             futures.add(phoneLookupFuture);
-            String eventName = onSuccessfulBulkUpdatedEventName(phoneLookup, isBuilt);
+            String eventName =
+                onSuccessfulBulkUpdatedEventName(phoneLookup.getLoggingName(), isBuilt);
             futureTimer.applyTiming(phoneLookupFuture, eventName);
           }
           ListenableFuture<Void> combinedFuture =
               Futures.transform(
                   Futures.allAsList(futures), unused -> null, lightweightExecutorService);
-          String eventName = onSuccessfulBulkUpdatedEventName(this, isBuilt);
+          String eventName = onSuccessfulBulkUpdatedEventName(getLoggingName(), isBuilt);
           futureTimer.applyTiming(combinedFuture, eventName);
           return combinedFuture;
         },
@@ -278,19 +278,23 @@
         Futures.allAsList(futures), unused -> null, lightweightExecutorService);
   }
 
-  private static String getMostRecentInfoEventName(Object classNameSource, boolean isBuilt) {
+  private static String getMostRecentInfoEventName(String loggingName, boolean isBuilt) {
     return String.format(
         !isBuilt
             ? Metrics.INITIAL_GET_MOST_RECENT_INFO_TEMPLATE
             : Metrics.GET_MOST_RECENT_INFO_TEMPLATE,
-        classNameSource.getClass().getSimpleName());
+        loggingName);
   }
 
-  private static String onSuccessfulBulkUpdatedEventName(Object classNameSource, boolean isBuilt) {
+  private static String onSuccessfulBulkUpdatedEventName(String loggingName, boolean isBuilt) {
     return String.format(
         !isBuilt
             ? Metrics.INITIAL_ON_SUCCESSFUL_BULK_UPDATE_TEMPLATE
             : Metrics.ON_SUCCESSFUL_BULK_UPDATE_TEMPLATE,
-        classNameSource.getClass().getSimpleName());
+        loggingName);
+  }
+
+  private String getLoggingName() {
+    return "CompositePhoneLookup";
   }
 }
diff --git a/java/com/android/dialer/phonelookup/consolidator/PhoneLookupInfoConsolidator.java b/java/com/android/dialer/phonelookup/consolidator/PhoneLookupInfoConsolidator.java
index 23ecc83..29a0de5 100644
--- a/java/com/android/dialer/phonelookup/consolidator/PhoneLookupInfoConsolidator.java
+++ b/java/com/android/dialer/phonelookup/consolidator/PhoneLookupInfoConsolidator.java
@@ -347,6 +347,14 @@
 
   /**
    * The {@link PhoneLookupInfo} passed to the constructor is associated with a number. This method
+   * returns whether the number is an emergency number (e.g., 911 in the U.S.).
+   */
+  public boolean isEmergencyNumber() {
+    return phoneLookupInfo.getEmergencyInfo().getIsEmergencyNumber();
+  }
+
+  /**
+   * The {@link PhoneLookupInfo} passed to the constructor is associated with a number. This method
    * returns whether the number can be reported as invalid.
    *
    * <p>As we currently report invalid numbers via the People API, only numbers from the People API
diff --git a/java/com/android/dialer/phonelookup/cp2/Cp2DefaultDirectoryPhoneLookup.java b/java/com/android/dialer/phonelookup/cp2/Cp2DefaultDirectoryPhoneLookup.java
index c5d4e53..1642f9b 100644
--- a/java/com/android/dialer/phonelookup/cp2/Cp2DefaultDirectoryPhoneLookup.java
+++ b/java/com/android/dialer/phonelookup/cp2/Cp2DefaultDirectoryPhoneLookup.java
@@ -644,6 +644,11 @@
         });
   }
 
+  @Override
+  public String getLoggingName() {
+    return "Cp2DefaultDirectoryPhoneLookup";
+  }
+
   /**
    * 1. get all contact ids. if the id is unset, add the number to the list of contacts to look up.
    * 2. reduce our list of contact ids to those that were updated after lastModified. 3. Now we have
diff --git a/java/com/android/dialer/phonelookup/cp2/Cp2ExtendedDirectoryPhoneLookup.java b/java/com/android/dialer/phonelookup/cp2/Cp2ExtendedDirectoryPhoneLookup.java
index 2b98f26..77a95e7 100644
--- a/java/com/android/dialer/phonelookup/cp2/Cp2ExtendedDirectoryPhoneLookup.java
+++ b/java/com/android/dialer/phonelookup/cp2/Cp2ExtendedDirectoryPhoneLookup.java
@@ -234,4 +234,9 @@
   public ListenableFuture<Void> clearData() {
     return Futures.immediateFuture(null);
   }
+
+  @Override
+  public String getLoggingName() {
+    return "Cp2ExtendedDirectoryPhoneLookup";
+  }
 }
diff --git a/java/com/android/dialer/phonelookup/emergency/EmergencyPhoneLookup.java b/java/com/android/dialer/phonelookup/emergency/EmergencyPhoneLookup.java
new file mode 100644
index 0000000..d31614a
--- /dev/null
+++ b/java/com/android/dialer/phonelookup/emergency/EmergencyPhoneLookup.java
@@ -0,0 +1,122 @@
+/*
+ * 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.phonelookup.emergency;
+
+import android.content.Context;
+import com.android.dialer.DialerPhoneNumber;
+import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor;
+import com.android.dialer.inject.ApplicationContext;
+import com.android.dialer.phonelookup.PhoneLookup;
+import com.android.dialer.phonelookup.PhoneLookupInfo;
+import com.android.dialer.phonelookup.PhoneLookupInfo.EmergencyInfo;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import javax.inject.Inject;
+
+/**
+ * PhoneLookup implementation for checking if a number is an emergency number.
+ *
+ * <p>The check has to be done in a PhoneLookup as it involves detecting the user's location and
+ * obtaining SIM info, which are expensive operations. Doing it in the main thread will make the UI
+ * super janky.
+ */
+public class EmergencyPhoneLookup implements PhoneLookup<EmergencyInfo> {
+
+  private final Context appContext;
+  private final ListeningExecutorService backgroundExecutorService;
+
+  @Inject
+  EmergencyPhoneLookup(
+      @ApplicationContext Context appContext,
+      @BackgroundExecutor ListeningExecutorService backgroundExecutorService) {
+    this.appContext = appContext;
+    this.backgroundExecutorService = backgroundExecutorService;
+  }
+
+  @Override
+  public ListenableFuture<EmergencyInfo> lookup(DialerPhoneNumber dialerPhoneNumber) {
+    return backgroundExecutorService.submit(
+        () ->
+            EmergencyInfo.newBuilder()
+                .setIsEmergencyNumber(
+                    PhoneNumberHelper.isLocalEmergencyNumber(
+                        appContext, dialerPhoneNumber.getNormalizedNumber()))
+                .build());
+  }
+
+  @Override
+  public ListenableFuture<Boolean> isDirty(ImmutableSet<DialerPhoneNumber> phoneNumbers) {
+    return Futures.immediateFuture(false);
+  }
+
+  @Override
+  public ListenableFuture<ImmutableMap<DialerPhoneNumber, EmergencyInfo>> getMostRecentInfo(
+      ImmutableMap<DialerPhoneNumber, EmergencyInfo> existingInfoMap) {
+    // We can update EmergencyInfo for all numbers in the provided map, but the negative impact on
+    // performance is intolerable as checking a single number involves detecting the user's location
+    // and obtaining SIM info, which will take more than 100ms (see
+    // android.telephony.PhoneNumberUtils#isLocalEmergencyNumber(Context, int, String) for details).
+    //
+    // As emergency numbers won't change in a country, the only case we will miss is that
+    //   (1) a number is an emergency number in country A but not in country B,
+    //   (2) a user has an emergency call entry when they are in country A, and
+    //   (3) they travel from A to B,
+    // which is a rare event.
+    //
+    // We can update the implementation if telecom supports batch check in the future.
+    return Futures.immediateFuture(existingInfoMap);
+  }
+
+  @Override
+  public void setSubMessage(PhoneLookupInfo.Builder destination, EmergencyInfo subMessage) {
+    destination.setEmergencyInfo(subMessage);
+  }
+
+  @Override
+  public EmergencyInfo getSubMessage(PhoneLookupInfo phoneLookupInfo) {
+    return phoneLookupInfo.getEmergencyInfo();
+  }
+
+  @Override
+  public ListenableFuture<Void> onSuccessfulBulkUpdate() {
+    return Futures.immediateFuture(null);
+  }
+
+  @Override
+  public void registerContentObservers() {
+    // No content observer to register.
+  }
+
+  @Override
+  public void unregisterContentObservers() {
+    // Nothing to be done as no content observer is registered.
+  }
+
+  @Override
+  public ListenableFuture<Void> clearData() {
+    return Futures.immediateFuture(null);
+  }
+
+  @Override
+  public String getLoggingName() {
+    return "EmergencyPhoneLookup";
+  }
+}
diff --git a/java/com/android/dialer/phonelookup/phone_lookup_info.proto b/java/com/android/dialer/phonelookup/phone_lookup_info.proto
index 50817c4..9e9dfa9 100644
--- a/java/com/android/dialer/phonelookup/phone_lookup_info.proto
+++ b/java/com/android/dialer/phonelookup/phone_lookup_info.proto
@@ -14,7 +14,7 @@
 // "cp2_info_in_default_directory" corresponds to class
 // Cp2DefaultDirectoryPhoneLookup, and class Cp2DefaultDirectoryPhoneLookup
 // alone is responsible for populating it.
-// Next ID: 9
+// Next ID: 10
 message PhoneLookupInfo {
   // Information about a PhoneNumber retrieved from CP2.
   message Cp2Info {
@@ -175,4 +175,11 @@
     optional string photo_uri = 3;
   }
   optional CequintInfo cequint_info = 8;
+
+  // Message indicating whether a number is an emergency number.
+  // Next ID: 2
+  message EmergencyInfo {
+    optional bool is_emergency_number = 1;
+  }
+  optional EmergencyInfo emergency_info = 9;
 }
\ No newline at end of file
diff --git a/java/com/android/dialer/phonelookup/spam/SpamPhoneLookup.java b/java/com/android/dialer/phonelookup/spam/SpamPhoneLookup.java
index 6b77036..7e5c973 100644
--- a/java/com/android/dialer/phonelookup/spam/SpamPhoneLookup.java
+++ b/java/com/android/dialer/phonelookup/spam/SpamPhoneLookup.java
@@ -27,7 +27,7 @@
 import com.android.dialer.phonelookup.PhoneLookupInfo;
 import com.android.dialer.phonelookup.PhoneLookupInfo.SpamInfo;
 import com.android.dialer.spam.Spam;
-import com.android.dialer.spam.SpamStatus;
+import com.android.dialer.spam.status.SpamStatus;
 import com.android.dialer.storage.Unencrypted;
 import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableMap;
@@ -167,4 +167,9 @@
           return null;
         });
   }
+
+  @Override
+  public String getLoggingName() {
+    return "SpamPhoneLookup";
+  }
 }
diff --git a/java/com/android/dialer/precall/PreCallCoordinator.java b/java/com/android/dialer/precall/PreCallCoordinator.java
index 9c24e0d..4d4859a 100644
--- a/java/com/android/dialer/precall/PreCallCoordinator.java
+++ b/java/com/android/dialer/precall/PreCallCoordinator.java
@@ -68,8 +68,8 @@
   @NonNull
   PendingAction startPendingAction();
 
-  <Output> void listen(
-      ListenableFuture<Output> future,
-      Consumer<Output> successListener,
+  <OutputT> void listen(
+      ListenableFuture<OutputT> future,
+      Consumer<OutputT> successListener,
       Consumer<Throwable> failureListener);
 }
diff --git a/java/com/android/dialer/precall/impl/DuoAction.java b/java/com/android/dialer/precall/impl/DuoAction.java
new file mode 100644
index 0000000..c05fbe1
--- /dev/null
+++ b/java/com/android/dialer/precall/impl/DuoAction.java
@@ -0,0 +1,97 @@
+/*
+ * 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.precall.impl;
+
+import android.content.Context;
+import android.content.Intent;
+import com.android.dialer.callintent.CallIntentBuilder;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.concurrent.Annotations.Ui;
+import com.android.dialer.duo.Duo.ReachabilityData;
+import com.android.dialer.duo.DuoComponent;
+import com.android.dialer.precall.PreCallAction;
+import com.android.dialer.precall.PreCallCoordinator;
+import com.android.dialer.precall.PreCallCoordinator.PendingAction;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import javax.inject.Inject;
+
+/**
+ * Checks if a duo call is actually callable, and request an activity for {@link
+ * android.app.Activity#startActivityForResult(Intent, int)}
+ */
+public class DuoAction implements PreCallAction {
+
+  private final ListeningExecutorService uiExecutor;
+
+  @Inject
+  DuoAction(@Ui ListeningExecutorService uiExecutor) {
+    this.uiExecutor = uiExecutor;
+  }
+
+  @Override
+  public boolean requiresUi(Context context, CallIntentBuilder builder) {
+    // Duo call must be started with startActivityForResult() which needs a activity.
+    return builder.isDuoCall();
+  }
+
+  @Override
+  public void runWithoutUi(Context context, CallIntentBuilder builder) {}
+
+  @Override
+  public void runWithUi(PreCallCoordinator coordinator) {
+    if (!requiresUi(coordinator.getActivity(), coordinator.getBuilder())) {
+      return;
+    }
+    String number = coordinator.getBuilder().getUri().getSchemeSpecificPart();
+    ListenableFuture<ImmutableMap<String, ReachabilityData>> reachabilities =
+        DuoComponent.get(coordinator.getActivity())
+            .getDuo()
+            .updateReachability(coordinator.getActivity(), ImmutableList.of(number));
+    PendingAction pendingAction = coordinator.startPendingAction();
+
+    Futures.addCallback(
+        reachabilities,
+        new FutureCallback<ImmutableMap<String, ReachabilityData>>() {
+          @Override
+          public void onSuccess(ImmutableMap<String, ReachabilityData> result) {
+            if (!result.containsKey(number) || !result.get(number).videoCallable()) {
+              LogUtil.w(
+                  "DuoAction.runWithUi",
+                  number + " number no longer duo reachable, falling back to carrier video call");
+              coordinator.getBuilder().setIsDuoCall(false);
+            }
+            pendingAction.finish();
+          }
+
+          @Override
+          public void onFailure(Throwable throwable) {
+            LogUtil.e("DuoAction.runWithUi", "reachability query failed", throwable);
+            coordinator.getBuilder().setIsDuoCall(false);
+            pendingAction.finish();
+          }
+        },
+        uiExecutor);
+  }
+
+  @Override
+  public void onDiscard() {}
+}
diff --git a/java/com/android/dialer/precall/impl/PreCallCoordinatorImpl.java b/java/com/android/dialer/precall/impl/PreCallCoordinatorImpl.java
index f2ff0e3..240549c 100644
--- a/java/com/android/dialer/precall/impl/PreCallCoordinatorImpl.java
+++ b/java/com/android/dialer/precall/impl/PreCallCoordinatorImpl.java
@@ -26,6 +26,7 @@
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.common.concurrent.DialerExecutorComponent;
 import com.android.dialer.common.concurrent.UiListener;
+import com.android.dialer.duo.DuoComponent;
 import com.android.dialer.function.Consumer;
 import com.android.dialer.logging.DialerImpression.Type;
 import com.android.dialer.logging.Logger;
@@ -33,6 +34,7 @@
 import com.android.dialer.precall.PreCallComponent;
 import com.android.dialer.precall.PreCallCoordinator;
 import com.android.dialer.telecom.TelecomUtil;
+import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -101,7 +103,7 @@
     LogUtil.enterBlock("PreCallCoordinatorImpl.runNextAction");
     Assert.checkArgument(currentAction == null);
     if (currentActionIndex >= actions.size()) {
-      TelecomUtil.placeCall(activity, builder.build());
+      placeCall();
       activity.finish();
       return;
     }
@@ -177,4 +179,20 @@
         output -> successListener.accept((OutputT) output),
         failureListener::accept);
   }
+
+  private void placeCall() {
+    if (builder.isDuoCall()) {
+      Optional<Intent> intent =
+          DuoComponent.get(activity)
+              .getDuo()
+              .getCallIntent(builder.getUri().getSchemeSpecificPart());
+      if (intent.isPresent()) {
+        activity.startActivityForResult(intent.get(), 0);
+        return;
+      } else {
+        LogUtil.e("PreCallCoordinatorImpl.placeCall", "duo.getCallIntent() returned absent");
+      }
+    }
+    TelecomUtil.placeCall(activity, builder.build());
+  }
 }
diff --git a/java/com/android/dialer/precall/impl/PreCallModule.java b/java/com/android/dialer/precall/impl/PreCallModule.java
index 9820e2b..455453e 100644
--- a/java/com/android/dialer/precall/impl/PreCallModule.java
+++ b/java/com/android/dialer/precall/impl/PreCallModule.java
@@ -37,12 +37,13 @@
   @Provides
   @Singleton
   public static ImmutableList<PreCallAction> provideActions(
-      CallingAccountSelector callingAccountSelector) {
+      DuoAction duoAction, CallingAccountSelector callingAccountSelector) {
     return ImmutableList.of(
         new PermissionCheckAction(),
         new MalformedNumberRectifier(
             ImmutableList.of(new UkRegionPrefixInInternationalFormatHandler())),
         callingAccountSelector,
+        duoAction,
         new AssistedDialAction());
   }
 }
diff --git a/java/com/android/dialer/searchfragment/list/NewSearchFragment.java b/java/com/android/dialer/searchfragment/list/NewSearchFragment.java
index f1eed91..abb3aec 100644
--- a/java/com/android/dialer/searchfragment/list/NewSearchFragment.java
+++ b/java/com/android/dialer/searchfragment/list/NewSearchFragment.java
@@ -51,9 +51,7 @@
 import com.android.dialer.common.FragmentUtils;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.common.concurrent.ThreadUtil;
-import com.android.dialer.constants.ActivityRequestCodes;
 import com.android.dialer.dialercontact.DialerContact;
-import com.android.dialer.duo.DuoComponent;
 import com.android.dialer.enrichedcall.EnrichedCallComponent;
 import com.android.dialer.enrichedcall.EnrichedCallManager.CapabilitiesListener;
 import com.android.dialer.logging.DialerImpression;
@@ -549,8 +547,11 @@
   public void placeDuoCall(String phoneNumber) {
     Logger.get(getContext())
         .logImpression(DialerImpression.Type.LIGHTBRINGER_VIDEO_REQUESTED_FROM_SEARCH);
-    Intent intent = DuoComponent.get(getContext()).getDuo().getCallIntent(phoneNumber).orNull();
-    getActivity().startActivityForResult(intent, ActivityRequestCodes.DIALTACTS_DUO);
+    PreCall.start(
+        getContext(),
+        new CallIntentBuilder(phoneNumber, CallInitiationType.Type.REGULAR_SEARCH)
+            .setIsVideoCall(true)
+            .setIsDuoCall(true));
     FragmentUtils.getParentUnsafe(this, SearchFragmentListener.class).onCallPlacedFromSearch();
   }
 
diff --git a/java/com/android/dialer/spam/Spam.java b/java/com/android/dialer/spam/Spam.java
index 21d770e..181a55d 100644
--- a/java/com/android/dialer/spam/Spam.java
+++ b/java/com/android/dialer/spam/Spam.java
@@ -24,6 +24,7 @@
 import com.android.dialer.logging.ContactLookupResult;
 import com.android.dialer.logging.ContactSource;
 import com.android.dialer.logging.ReportingLocation;
+import com.android.dialer.spam.status.SpamStatus;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -41,6 +42,25 @@
       ImmutableSet<DialerPhoneNumber> dialerPhoneNumbers);
 
   /**
+   * Checks if the given number is suspected of being spam.
+   *
+   * @param dialerPhoneNumber the phone number.
+   * @return the {@link SpamStatus} for the given number.
+   */
+  ListenableFuture<SpamStatus> checkSpamStatus(DialerPhoneNumber dialerPhoneNumber);
+
+  /**
+   * Checks if the given number is suspected of being spam.
+   *
+   * <p>See {@link #checkSpamStatus(DialerPhoneNumber)}.
+   *
+   * @param number the phone number.
+   * @param defaultCountryIso the default country to use if it's not part of the number.
+   * @return the {@link SpamStatus} for the given number.
+   */
+  ListenableFuture<SpamStatus> checkSpamStatus(String number, @Nullable String defaultCountryIso);
+
+  /**
    * Called as an indication that the Spam implementation should check whether downloading a spam
    * list needs to occur or not.
    *
@@ -55,15 +75,6 @@
   ListenableFuture<Void> updateSpamListDownload(boolean isEnabledByUser);
 
   /**
-   * Checks if the given number is suspected of being a spam.
-   *
-   * @param number The phone number of the call.
-   * @param countryIso The country ISO of the call.
-   * @param listener The callback to be invoked after {@code Info} is fetched.
-   */
-  void checkSpamStatus(String number, String countryIso, Listener listener);
-
-  /**
    * @param number The number to check if the number is in the user's white list (non spam list)
    * @param countryIso The country ISO of the call.
    * @param listener The callback to be invoked after {@code Info} is fetched.
diff --git a/java/com/android/dialer/spam/SpamComponent.java b/java/com/android/dialer/spam/SpamComponent.java
index 2b70b6f..a0ffcce 100644
--- a/java/com/android/dialer/spam/SpamComponent.java
+++ b/java/com/android/dialer/spam/SpamComponent.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import com.android.dialer.inject.HasRootComponent;
+import com.android.dialer.inject.IncludeInDialerRoot;
 import dagger.Subcomponent;
 
 /** Dagger component to get Spam. */
@@ -35,6 +36,7 @@
   }
 
   /** Used to refer to the root application component. */
+  @IncludeInDialerRoot
   public interface HasComponent {
     SpamComponent spamComponent();
   }
diff --git a/java/com/android/dialer/spam/SpamStub.java b/java/com/android/dialer/spam/SpamStub.java
index 8851fd0..2789c01 100644
--- a/java/com/android/dialer/spam/SpamStub.java
+++ b/java/com/android/dialer/spam/SpamStub.java
@@ -16,12 +16,14 @@
 
 package com.android.dialer.spam;
 
+import android.support.annotation.Nullable;
 import com.android.dialer.DialerPhoneNumber;
 import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor;
 import com.android.dialer.logging.ContactLookupResult;
 import com.android.dialer.logging.ContactSource;
 import com.android.dialer.logging.ReportingLocation;
-import com.google.common.base.Optional;
+import com.android.dialer.spam.status.SimpleSpamStatus;
+import com.android.dialer.spam.status.SpamStatus;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.util.concurrent.Futures;
@@ -47,33 +49,27 @@
           ImmutableMap.Builder<DialerPhoneNumber, SpamStatus> resultBuilder =
               new ImmutableMap.Builder<>();
           for (DialerPhoneNumber dialerPhoneNumber : dialerPhoneNumbers) {
-            resultBuilder.put(
-                dialerPhoneNumber,
-                new SpamStatus() {
-                  @Override
-                  public boolean isSpam() {
-                    return false;
-                  }
-
-                  @Override
-                  public Optional<Long> getTimestampMillis() {
-                    return Optional.absent();
-                  }
-                });
+            resultBuilder.put(dialerPhoneNumber, SimpleSpamStatus.notSpam());
           }
           return resultBuilder.build();
         });
   }
 
   @Override
-  public ListenableFuture<Void> updateSpamListDownload(boolean isEnabledByUser) {
-    // no-op
-    return Futures.immediateFuture(null);
+  public ListenableFuture<SpamStatus> checkSpamStatus(DialerPhoneNumber dialerPhoneNumber) {
+    return Futures.immediateFuture(SimpleSpamStatus.notSpam());
   }
 
   @Override
-  public void checkSpamStatus(String number, String countryIso, Listener listener) {
-    listener.onComplete(false);
+  public ListenableFuture<SpamStatus> checkSpamStatus(
+      String number, @Nullable String defaultCountryIso) {
+    return Futures.immediateFuture(SimpleSpamStatus.notSpam());
+  }
+
+  @Override
+  public ListenableFuture<Void> updateSpamListDownload(boolean isEnabledByUser) {
+    // no-op
+    return Futures.immediateFuture(null);
   }
 
   @Override
diff --git a/java/com/android/dialer/spam/StubSpamModule.java b/java/com/android/dialer/spam/StubSpamModule.java
index 5540408..b609674 100644
--- a/java/com/android/dialer/spam/StubSpamModule.java
+++ b/java/com/android/dialer/spam/StubSpamModule.java
@@ -16,10 +16,13 @@
 
 package com.android.dialer.spam;
 
+import com.android.dialer.inject.DialerVariant;
+import com.android.dialer.inject.InstallIn;
 import dagger.Binds;
 import dagger.Module;
 
 /** Module which binds {@link SpamStub}. */
+@InstallIn(variants = {DialerVariant.DIALER_TEST})
 @Module
 public abstract class StubSpamModule {
 
diff --git a/java/com/android/dialer/spam/promo/SpamBlockingPromoHelper.java b/java/com/android/dialer/spam/promo/SpamBlockingPromoHelper.java
index a117e19..42fb39f 100644
--- a/java/com/android/dialer/spam/promo/SpamBlockingPromoHelper.java
+++ b/java/com/android/dialer/spam/promo/SpamBlockingPromoHelper.java
@@ -42,6 +42,8 @@
   static final String SPAM_BLOCKING_PROMO_PERIOD_MILLIS = "spam_blocking_promo_period_millis";
   static final String SPAM_BLOCKING_PROMO_LAST_SHOW_MILLIS = "spam_blocking_promo_last_show_millis";
   public static final String ENABLE_SPAM_BLOCKING_PROMO = "enable_spam_blocking_promo";
+  public static final String ENABLE_AFTER_CALL_SPAM_BLOCKING_PROMO =
+      "enable_after_call_spam_blocking_promo";
 
   private final Context context;
   private final SpamSettings spamSettings;
@@ -77,6 +79,13 @@
     return lastShowMillis == 0 || System.currentTimeMillis() - lastShowMillis > showPeriodMillis;
   }
 
+  /* Returns true if we should show a spam blocking promo in after call notification scenario. */
+  public boolean shouldShowAfterCallSpamBlockingPromo() {
+    return shouldShowSpamBlockingPromo()
+        && ConfigProviderBindings.get(context)
+            .getBoolean(ENABLE_AFTER_CALL_SPAM_BLOCKING_PROMO, false);
+  }
+
   /**
    * Shows a spam blocking promo dialog.
    *
diff --git a/java/com/android/dialer/spam/status/GlobalSpamListStatus.java b/java/com/android/dialer/spam/status/GlobalSpamListStatus.java
new file mode 100644
index 0000000..741d6e8
--- /dev/null
+++ b/java/com/android/dialer/spam/status/GlobalSpamListStatus.java
@@ -0,0 +1,52 @@
+/*
+ * 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.spam.status;
+
+import android.support.annotation.IntDef;
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Optional;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** A value class representing a number's spam status in the global spam list. */
+@AutoValue
+public abstract class GlobalSpamListStatus {
+
+  /** Integers representing the spam status in the global spam list. */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({Status.NOT_ON_LIST, Status.ON_LIST})
+  public @interface Status {
+    int NOT_ON_LIST = 1;
+    int ON_LIST = 2;
+  }
+
+  public abstract @Status int getStatus();
+
+  /**
+   * Returns the timestamp (in milliseconds) representing when a number's spam status was put on the
+   * list, or {@code Optional.absent()} if the number is not on the list.
+   */
+  public abstract Optional<Long> getTimestampMillis();
+
+  public static GlobalSpamListStatus notOnList() {
+    return new AutoValue_GlobalSpamListStatus(Status.NOT_ON_LIST, Optional.absent());
+  }
+
+  public static GlobalSpamListStatus onList(long timestampMillis) {
+    return new AutoValue_GlobalSpamListStatus(Status.ON_LIST, Optional.of(timestampMillis));
+  }
+}
diff --git a/java/com/android/dialer/spam/status/SimpleSpamStatus.java b/java/com/android/dialer/spam/status/SimpleSpamStatus.java
new file mode 100644
index 0000000..ec28b9d
--- /dev/null
+++ b/java/com/android/dialer/spam/status/SimpleSpamStatus.java
@@ -0,0 +1,36 @@
+/*
+ * 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.spam.status;
+
+import android.support.annotation.Nullable;
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Optional;
+
+/** Holds a boolean and long to represent spam status. */
+@AutoValue
+public abstract class SimpleSpamStatus implements SpamStatus {
+
+  /** Returns a SimpleSpamStatus with the given boolean and timestamp. */
+  public static SimpleSpamStatus create(boolean isSpam, @Nullable Long timestampMillis) {
+    return new AutoValue_SimpleSpamStatus(isSpam, Optional.fromNullable(timestampMillis));
+  }
+
+  /** Returns a SimpleSpamStatus that's not marked as spam and has no timestamp. */
+  public static SimpleSpamStatus notSpam() {
+    return create(false, null);
+  }
+}
diff --git a/java/com/android/dialer/spam/SpamStatus.java b/java/com/android/dialer/spam/status/SpamStatus.java
similarity index 96%
rename from java/com/android/dialer/spam/SpamStatus.java
rename to java/com/android/dialer/spam/status/SpamStatus.java
index 0b859d1..8186ac5 100644
--- a/java/com/android/dialer/spam/SpamStatus.java
+++ b/java/com/android/dialer/spam/status/SpamStatus.java
@@ -14,7 +14,7 @@
  * limitations under the License
  */
 
-package com.android.dialer.spam;
+package com.android.dialer.spam.status;
 
 import com.google.common.base.Optional;
 
diff --git a/java/com/android/dialer/spam/status/UserSpamListStatus.java b/java/com/android/dialer/spam/status/UserSpamListStatus.java
new file mode 100644
index 0000000..01f9987
--- /dev/null
+++ b/java/com/android/dialer/spam/status/UserSpamListStatus.java
@@ -0,0 +1,58 @@
+/*
+ * 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.spam.status;
+
+import android.support.annotation.IntDef;
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Optional;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** A value class representing a number's spam status in the user spam list. */
+@AutoValue
+@SuppressWarnings("Guava")
+public abstract class UserSpamListStatus {
+
+  /** Integers representing the spam status in the user spam list. */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({Status.NOT_ON_LIST, Status.WHITELISTED, Status.BLACKLISTED})
+  public @interface Status {
+    int NOT_ON_LIST = 1;
+    int WHITELISTED = 2;
+    int BLACKLISTED = 3;
+  }
+
+  public abstract @Status int getStatus();
+
+  /**
+   * Returns the timestamp (in milliseconds) representing when a number's spam status was put on the
+   * list, or {@code Optional.absent()} if the number is not on the list.
+   */
+  public abstract Optional<Long> getTimestampMillis();
+
+  public static UserSpamListStatus notOnList() {
+    return new AutoValue_UserSpamListStatus(Status.NOT_ON_LIST, Optional.absent());
+  }
+
+  public static UserSpamListStatus whitelisted(long timestampMillis) {
+    return new AutoValue_UserSpamListStatus(Status.WHITELISTED, Optional.of(timestampMillis));
+  }
+
+  public static UserSpamListStatus blacklisted(long timestampMillis) {
+    return new AutoValue_UserSpamListStatus(Status.BLACKLISTED, Optional.of(timestampMillis));
+  }
+}
diff --git a/java/com/android/dialer/speeddial/DisambigDialog.java b/java/com/android/dialer/speeddial/DisambigDialog.java
index 98fc992..2dd43fa 100644
--- a/java/com/android/dialer/speeddial/DisambigDialog.java
+++ b/java/com/android/dialer/speeddial/DisambigDialog.java
@@ -18,7 +18,6 @@
 
 import android.app.Dialog;
 import android.content.Context;
-import android.content.Intent;
 import android.os.Bundle;
 import android.support.annotation.VisibleForTesting;
 import android.support.annotation.WorkerThread;
@@ -40,8 +39,6 @@
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.common.concurrent.DefaultFutureCallback;
 import com.android.dialer.common.concurrent.DialerExecutorComponent;
-import com.android.dialer.constants.ActivityRequestCodes;
-import com.android.dialer.duo.DuoComponent;
 import com.android.dialer.logging.DialerImpression;
 import com.android.dialer.logging.Logger;
 import com.android.dialer.precall.PreCall;
@@ -184,16 +181,13 @@
       Logger.get(getContext())
           .logImpression(
               DialerImpression.Type.LIGHTBRINGER_VIDEO_REQUESTED_FOR_FAVORITE_CONTACT_DISAMBIG);
-      Intent intent =
-          DuoComponent.get(getContext()).getDuo().getCallIntent(channel.number()).orNull();
-      getActivity().startActivityForResult(intent, ActivityRequestCodes.DIALTACTS_DUO);
-      return;
     }
 
     PreCall.start(
         getContext(),
         new CallIntentBuilder(channel.number(), CallInitiationType.Type.SPEED_DIAL)
-            .setIsVideoCall(true));
+            .setIsVideoCall(true)
+            .setIsDuoCall(channel.technology() == Channel.DUO));
     dismiss();
   }
 
diff --git a/java/com/android/dialer/speeddial/SpeedDialFragment.java b/java/com/android/dialer/speeddial/SpeedDialFragment.java
index aa306d2..fac9a13 100644
--- a/java/com/android/dialer/speeddial/SpeedDialFragment.java
+++ b/java/com/android/dialer/speeddial/SpeedDialFragment.java
@@ -41,7 +41,6 @@
 import com.android.dialer.common.concurrent.DialerExecutorComponent;
 import com.android.dialer.common.concurrent.SupportUiListener;
 import com.android.dialer.constants.ActivityRequestCodes;
-import com.android.dialer.duo.DuoComponent;
 import com.android.dialer.historyitemactions.DividerModule;
 import com.android.dialer.historyitemactions.HistoryItemActionBottomSheet;
 import com.android.dialer.historyitemactions.HistoryItemActionModule;
@@ -243,16 +242,13 @@
       if (channel.technology() == Channel.DUO) {
         Logger.get(activity)
             .logImpression(DialerImpression.Type.LIGHTBRINGER_VIDEO_REQUESTED_FOR_FAVORITE_CONTACT);
-        Intent intent =
-            DuoComponent.get(activity).getDuo().getCallIntent(channel.number()).orNull();
-        activity.startActivityForResult(intent, ActivityRequestCodes.DIALTACTS_DUO);
-        return;
       }
 
       PreCall.start(
           activity,
           new CallIntentBuilder(channel.number(), CallInitiationType.Type.SPEED_DIAL)
-              .setIsVideoCall(channel.isVideoTechnology()));
+              .setIsVideoCall(channel.isVideoTechnology())
+              .setIsDuoCall(channel.technology() == Channel.DUO));
     }
 
     @Override
@@ -341,15 +337,12 @@
         Logger.get(getContext())
             .logImpression(
                 DialerImpression.Type.LIGHTBRINGER_VIDEO_REQUESTED_FOR_SUGGESTED_CONTACT);
-        Intent intent =
-            DuoComponent.get(getContext()).getDuo().getCallIntent(channel.number()).orNull();
-        getActivity().startActivityForResult(intent, ActivityRequestCodes.DIALTACTS_DUO);
-        return;
       }
       PreCall.start(
           getContext(),
           new CallIntentBuilder(channel.number(), CallInitiationType.Type.SPEED_DIAL)
-              .setIsVideoCall(channel.isVideoTechnology()));
+              .setIsVideoCall(channel.isVideoTechnology())
+              .setIsDuoCall(channel.technology() == Channel.DUO));
     }
 
     private final class StarContactModule implements HistoryItemActionModule {
@@ -426,15 +419,12 @@
       if (channel.technology() == Channel.DUO) {
         Logger.get(activity)
             .logImpression(DialerImpression.Type.LIGHTBRINGER_VIDEO_REQUESTED_FOR_FAVORITE_CONTACT);
-        Intent intent =
-            DuoComponent.get(activity).getDuo().getCallIntent(channel.number()).orNull();
-        activity.startActivityForResult(intent, ActivityRequestCodes.DIALTACTS_DUO);
-        return;
       }
       PreCall.start(
           activity,
           new CallIntentBuilder(channel.number(), CallInitiationType.Type.SPEED_DIAL)
-              .setIsVideoCall(channel.isVideoTechnology()));
+              .setIsVideoCall(channel.isVideoTechnology())
+              .setIsDuoCall(channel.technology() == Channel.DUO));
     }
 
     @Override
diff --git a/java/com/android/dialer/storage/StorageComponent.java b/java/com/android/dialer/storage/StorageComponent.java
index cb5c4a8..4754cc6 100644
--- a/java/com/android/dialer/storage/StorageComponent.java
+++ b/java/com/android/dialer/storage/StorageComponent.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.content.SharedPreferences;
 import com.android.dialer.inject.HasRootComponent;
+import com.android.dialer.inject.IncludeInDialerRoot;
 import dagger.Subcomponent;
 
 /** Dagger component for storage. */
@@ -42,6 +43,7 @@
   }
 
   /** Used to refer to the root application component. */
+  @IncludeInDialerRoot
   public interface HasComponent {
     StorageComponent storageComponent();
   }
diff --git a/java/com/android/dialer/storage/StorageModule.java b/java/com/android/dialer/storage/StorageModule.java
index e1c5b4b..370998c 100644
--- a/java/com/android/dialer/storage/StorageModule.java
+++ b/java/com/android/dialer/storage/StorageModule.java
@@ -20,11 +20,14 @@
 import android.preference.PreferenceManager;
 import android.support.v4.content.ContextCompat;
 import com.android.dialer.inject.ApplicationContext;
+import com.android.dialer.inject.DialerVariant;
+import com.android.dialer.inject.InstallIn;
 import dagger.Module;
 import dagger.Provides;
 import javax.inject.Singleton;
 
 /** Module for the storage component. */
+@InstallIn(variants = {DialerVariant.DIALER_TEST})
 @Module
 public class StorageModule {
 
diff --git a/java/com/android/dialer/voicemail/listui/menu/Modules.java b/java/com/android/dialer/voicemail/listui/menu/Modules.java
index dcd9116..5a9a711 100644
--- a/java/com/android/dialer/voicemail/listui/menu/Modules.java
+++ b/java/com/android/dialer/voicemail/listui/menu/Modules.java
@@ -58,6 +58,7 @@
         .setCanSupportCarrierVideoCall(
             voicemailEntry.getNumberAttributes().getCanSupportCarrierVideoCall())
         .setIsBlocked(voicemailEntry.getNumberAttributes().getIsBlocked())
+        .setIsEmergencyNumber(voicemailEntry.getNumberAttributes().getIsEmergencyNumber())
         .setIsSpam(voicemailEntry.getNumberAttributes().getIsSpam())
         // A voicemail call is an outgoing call to the voicemail box.
         // Voicemail entries are not voicemail calls.
diff --git a/java/com/android/incallui/AnswerScreenPresenter.java b/java/com/android/incallui/AnswerScreenPresenter.java
index 0b79e4b..e41bac6 100644
--- a/java/com/android/incallui/AnswerScreenPresenter.java
+++ b/java/com/android/incallui/AnswerScreenPresenter.java
@@ -24,6 +24,7 @@
 import android.telecom.VideoProfile;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.concurrent.DialerExecutorComponent;
 import com.android.dialer.common.concurrent.ThreadUtil;
 import com.android.dialer.logging.DialerImpression;
 import com.android.dialer.logging.Logger;
@@ -35,6 +36,9 @@
 import com.android.incallui.call.DialerCall;
 import com.android.incallui.call.DialerCallListener;
 import com.android.incallui.incalluilock.InCallUiLock;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 
 /** Manages changes for an incoming call screen. */
 public class AnswerScreenPresenter
@@ -90,6 +94,39 @@
 
   @Override
   public void onAnswer(boolean answerVideoAsAudio) {
+
+    DialerCall incomingCall = CallList.getInstance().getIncomingCall();
+    InCallActivity inCallActivity =
+        (InCallActivity) answerScreen.getAnswerScreenFragment().getActivity();
+    ListenableFuture<Void> answerPrecondition;
+
+    if (incomingCall != null && inCallActivity != null) {
+      answerPrecondition = inCallActivity.getSpeakEasyCallManager().onNewIncomingCall(incomingCall);
+    } else {
+      answerPrecondition = Futures.immediateFuture(null);
+    }
+
+    Futures.addCallback(
+        answerPrecondition,
+        new FutureCallback<Void>() {
+          @Override
+          public void onSuccess(Void result) {
+            onAnswerCallback(answerVideoAsAudio);
+          }
+
+          @Override
+          public void onFailure(Throwable t) {
+            onAnswerCallback(answerVideoAsAudio);
+            // TODO(erfanian): Enumerate all error states and specify recovery strategies.
+            throw new RuntimeException("Failed to successfully complete pre call tasks.", t);
+          }
+        },
+        DialerExecutorComponent.get(context).uiExecutor());
+    addTimeoutCheck();
+  }
+
+  private void onAnswerCallback(boolean answerVideoAsAudio) {
+
     if (answerScreen.isVideoUpgradeRequest()) {
       if (answerVideoAsAudio) {
         Logger.get(context)
@@ -113,7 +150,6 @@
         call.answer();
       }
     }
-    addTimeoutCheck();
   }
 
   @Override
diff --git a/java/com/android/incallui/NotificationBroadcastReceiver.java b/java/com/android/incallui/NotificationBroadcastReceiver.java
index 52d01f5..602eb5c 100644
--- a/java/com/android/incallui/NotificationBroadcastReceiver.java
+++ b/java/com/android/incallui/NotificationBroadcastReceiver.java
@@ -20,15 +20,21 @@
 import android.content.Context;
 import android.content.Intent;
 import android.os.Build.VERSION_CODES;
+import android.support.annotation.NonNull;
 import android.support.annotation.RequiresApi;
 import android.telecom.CallAudioState;
 import android.telecom.VideoProfile;
 import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.concurrent.DialerExecutorComponent;
 import com.android.dialer.logging.DialerImpression;
 import com.android.dialer.logging.Logger;
 import com.android.incallui.call.CallList;
 import com.android.incallui.call.DialerCall;
 import com.android.incallui.call.TelecomAdapter;
+import com.android.incallui.speakeasy.SpeakEasyCallManager;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 
 /**
  * Accepts broadcast Intents which will be prepared by {@link StatusBarNotifier} and thus sent from
@@ -72,9 +78,9 @@
 
     // TODO: Commands of this nature should exist in the CallList.
     if (action.equals(ACTION_ANSWER_VIDEO_INCOMING_CALL)) {
-      answerIncomingCall(VideoProfile.STATE_BIDIRECTIONAL);
+      answerIncomingCall(VideoProfile.STATE_BIDIRECTIONAL, context);
     } else if (action.equals(ACTION_ANSWER_VOICE_INCOMING_CALL)) {
-      answerIncomingCall(VideoProfile.STATE_AUDIO_ONLY);
+      answerIncomingCall(VideoProfile.STATE_AUDIO_ONLY, context);
     } else if (action.equals(ACTION_DECLINE_INCOMING_CALL)) {
       Logger.get(context)
           .logImpression(DialerImpression.Type.REJECT_INCOMING_CALL_FROM_NOTIFICATION);
@@ -140,7 +146,7 @@
     }
   }
 
-  private void answerIncomingCall(int videoState) {
+  private void answerIncomingCall(int videoState, @NonNull Context context) {
     CallList callList = InCallPresenter.getInstance().getCallList();
     if (callList == null) {
       StatusBarNotifier.clearAllCallNotifications();
@@ -148,13 +154,42 @@
     } else {
       DialerCall call = callList.getIncomingCall();
       if (call != null) {
-        call.answer(videoState);
-        InCallPresenter.getInstance()
-            .showInCall(false /* showDialpad */, false /* newOutgoingCall */);
+
+        SpeakEasyCallManager speakEasyCallManager =
+            InCallPresenter.getInstance().getSpeakEasyCallManager();
+        ListenableFuture<Void> answerPrecondition;
+
+        if (speakEasyCallManager != null) {
+          answerPrecondition = speakEasyCallManager.onNewIncomingCall(call);
+        } else {
+          answerPrecondition = Futures.immediateFuture(null);
+        }
+
+        Futures.addCallback(
+            answerPrecondition,
+            new FutureCallback<Void>() {
+              @Override
+              public void onSuccess(Void result) {
+                answerIncomingCallCallback(call, videoState);
+              }
+
+              @Override
+              public void onFailure(Throwable t) {
+                answerIncomingCallCallback(call, videoState);
+                // TODO(erfanian): Enumerate all error states and specify recovery strategies.
+                throw new RuntimeException("Failed to successfully complete pre call tasks.", t);
+              }
+            },
+            DialerExecutorComponent.get(context).uiExecutor());
       }
     }
   }
 
+  private void answerIncomingCallCallback(@NonNull DialerCall call, int videoState) {
+    call.answer(videoState);
+    InCallPresenter.getInstance().showInCall(false /* showDialpad */, false /* newOutgoingCall */);
+  }
+
   private void declineIncomingCall() {
     CallList callList = InCallPresenter.getInstance().getCallList();
     if (callList == null) {
diff --git a/java/com/android/incallui/ReturnToCallController.java b/java/com/android/incallui/ReturnToCallController.java
index 96bdda1..7c4585c 100644
--- a/java/com/android/incallui/ReturnToCallController.java
+++ b/java/com/android/incallui/ReturnToCallController.java
@@ -196,12 +196,13 @@
         newInCallState != inCallState
             && newInCallState == InCallState.OUTGOING
             && shouldStartInBubbleMode;
+    boolean bubbleNeverVisible = (bubble == null || !(bubble.isVisible() || bubble.isDismissed()));
     if (bubble != null && isNewBackgroundCall) {
       // If new outgoing call is in bubble mode, update bubble info.
       // We don't update if new call is not in bubble mode even if the existing call is.
       bubble.setBubbleInfo(generateBubbleInfoForBackgroundCalling());
     }
-    if ((bubble == null || !(bubble.isVisible() || bubble.isDismissed()) || isNewBackgroundCall)
+    if (((bubbleNeverVisible && newInCallState != InCallState.OUTGOING) || isNewBackgroundCall)
         && getCall() != null
         && !InCallPresenter.getInstance().isShowingInCallUi()) {
       LogUtil.i("ReturnToCallController.onCallListChange", "going to show bubble");
diff --git a/java/com/android/incallui/call/CallList.java b/java/com/android/incallui/call/CallList.java
index 5d9db32..0e89ac7 100644
--- a/java/com/android/incallui/call/CallList.java
+++ b/java/com/android/incallui/call/CallList.java
@@ -32,6 +32,7 @@
 import com.android.dialer.blocking.FilteredNumbersUtil;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.concurrent.DialerExecutorComponent;
 import com.android.dialer.enrichedcall.EnrichedCallComponent;
 import com.android.dialer.enrichedcall.EnrichedCallManager;
 import com.android.dialer.logging.DialerImpression;
@@ -41,10 +42,14 @@
 import com.android.dialer.shortcuts.ShortcutUsageReporter;
 import com.android.dialer.spam.Spam;
 import com.android.dialer.spam.SpamComponent;
+import com.android.dialer.spam.status.SpamStatus;
 import com.android.dialer.telecom.TelecomCallUtil;
 import com.android.incallui.call.state.DialerCallState;
 import com.android.incallui.latencyreport.LatencyReport;
 import com.android.incallui.videotech.utils.SessionModificationState;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Iterator;
@@ -146,46 +151,53 @@
     LogUtil.d("CallList.onCallAdded", "callState=" + call.getState());
     if (SpamComponent.get(context).spamSettings().isSpamEnabled()) {
       String number = TelecomCallUtil.getNumber(telecomCall);
-      SpamComponent.get(context)
-          .spam()
-          .checkSpamStatus(
-              number,
-              call.getCountryIso(),
-              new Spam.Listener() {
-                @Override
-                public void onComplete(boolean isSpam) {
-                  boolean isIncomingCall =
-                      call.getState() == DialerCallState.INCOMING
-                          || call.getState() == DialerCallState.CALL_WAITING;
-                  if (isSpam) {
-                    if (!isIncomingCall) {
-                      LogUtil.i(
-                          "CallList.onCallAdded",
-                          "marking spam call as not spam because it's not an incoming call");
-                      isSpam = false;
-                    } else if (isPotentialEmergencyCallback(context, call)) {
-                      LogUtil.i(
-                          "CallList.onCallAdded",
-                          "marking spam call as not spam because an emergency call was made on this"
-                              + " device recently");
-                      isSpam = false;
-                    }
-                  }
+      ListenableFuture<SpamStatus> futureSpamStatus =
+          SpamComponent.get(context).spam().checkSpamStatus(number, call.getCountryIso());
 
-                  if (isIncomingCall) {
-                    Logger.get(context)
-                        .logCallImpression(
-                            isSpam
-                                ? DialerImpression.Type.INCOMING_SPAM_CALL
-                                : DialerImpression.Type.INCOMING_NON_SPAM_CALL,
-                            call.getUniqueCallId(),
-                            call.getTimeAddedMs());
-                  }
-                  call.setSpam(isSpam);
-                  onUpdateCall(call);
-                  notifyGenericListeners();
+      Futures.addCallback(
+          futureSpamStatus,
+          new FutureCallback<SpamStatus>() {
+            @Override
+            public void onSuccess(@Nullable SpamStatus result) {
+              boolean isIncomingCall =
+                  call.getState() == DialerCallState.INCOMING
+                      || call.getState() == DialerCallState.CALL_WAITING;
+              boolean isSpam = result.isSpam();
+              if (isSpam) {
+                if (!isIncomingCall) {
+                  LogUtil.i(
+                      "CallList.onCallAdded",
+                      "marking spam call as not spam because it's not an incoming call");
+                  isSpam = false;
+                } else if (isPotentialEmergencyCallback(context, call)) {
+                  LogUtil.i(
+                      "CallList.onCallAdded",
+                      "marking spam call as not spam because an emergency call was made on this"
+                          + " device recently");
+                  isSpam = false;
                 }
-              });
+              }
+
+              if (isIncomingCall) {
+                Logger.get(context)
+                    .logCallImpression(
+                        isSpam
+                            ? DialerImpression.Type.INCOMING_SPAM_CALL
+                            : DialerImpression.Type.INCOMING_NON_SPAM_CALL,
+                        call.getUniqueCallId(),
+                        call.getTimeAddedMs());
+              }
+              call.setSpam(isSpam);
+              onUpdateCall(call);
+              notifyGenericListeners();
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+              LogUtil.e("CallList.onFailure", "unable to query spam status", t);
+            }
+          },
+          DialerExecutorComponent.get(context).uiExecutor());
 
       Trace.beginSection("updateUserMarkedSpamStatus");
       updateUserMarkedSpamStatus(call, context, number);
diff --git a/java/com/android/incallui/contactgrid/ContactGridManager.java b/java/com/android/incallui/contactgrid/ContactGridManager.java
index d8b1f50..493f2d5 100644
--- a/java/com/android/incallui/contactgrid/ContactGridManager.java
+++ b/java/com/android/incallui/contactgrid/ContactGridManager.java
@@ -23,6 +23,8 @@
 import android.support.annotation.Nullable;
 import android.support.v4.view.ViewCompat;
 import android.telephony.PhoneNumberUtils;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
 import android.text.TextUtils;
 import android.view.View;
 import android.view.accessibility.AccessibilityEvent;
@@ -416,7 +418,9 @@
     // This is used for carriers like Project Fi to show the callback number for emergency calls.
     deviceNumberTextView.setText(
         context.getString(
-            R.string.contact_grid_callback_number, primaryCallState.callbackNumber()));
+            R.string.contact_grid_callback_number,
+            BidiFormatter.getInstance()
+                .unicodeWrap(primaryCallState.callbackNumber(), TextDirectionHeuristics.LTR)));
     deviceNumberTextView.setVisibility(View.VISIBLE);
     if (primaryInfo.shouldShowLocation()) {
       deviceNumberDivider.setVisibility(View.VISIBLE);
diff --git a/java/com/android/incallui/spam/SpamNotificationActivity.java b/java/com/android/incallui/spam/SpamNotificationActivity.java
index e10dea3..2cf4868 100644
--- a/java/com/android/incallui/spam/SpamNotificationActivity.java
+++ b/java/com/android/incallui/spam/SpamNotificationActivity.java
@@ -528,7 +528,7 @@
   }
 
   private void maybeShowSpamBlockingPromoAndFinish() {
-    if (!spamBlockingPromoHelper.shouldShowSpamBlockingPromo()) {
+    if (!spamBlockingPromoHelper.shouldShowAfterCallSpamBlockingPromo()) {
       finish();
       return;
     }
diff --git a/java/com/android/incallui/spam/SpamNotificationService.java b/java/com/android/incallui/spam/SpamNotificationService.java
index b418ea2..82a943d 100644
--- a/java/com/android/incallui/spam/SpamNotificationService.java
+++ b/java/com/android/incallui/spam/SpamNotificationService.java
@@ -122,7 +122,7 @@
                 ReportingLocation.Type.FEEDBACK_PROMPT,
                 contactLookupResultType);
         new FilteredNumberAsyncQueryHandler(this).blockNumber(null, number, countryIso);
-        if (spamBlockingPromoHelper.shouldShowSpamBlockingPromo()) {
+        if (spamBlockingPromoHelper.shouldShowAfterCallSpamBlockingPromo()) {
           spamBlockingPromoHelper.showSpamBlockingPromoNotification(
               notificationTag,
               notificationId,
diff --git a/java/com/android/incallui/speakeasy/SpeakEasyCallManager.java b/java/com/android/incallui/speakeasy/SpeakEasyCallManager.java
index 8a815d3..b060f64 100644
--- a/java/com/android/incallui/speakeasy/SpeakEasyCallManager.java
+++ b/java/com/android/incallui/speakeasy/SpeakEasyCallManager.java
@@ -21,6 +21,7 @@
 import android.support.v4.app.Fragment;
 import com.android.incallui.call.DialerCall;
 import com.google.common.base.Optional;
+import com.google.common.util.concurrent.ListenableFuture;
 
 /** Provides operations necessary to SpeakEasy. */
 public interface SpeakEasyCallManager {
@@ -40,6 +41,13 @@
   void onCallRemoved(@NonNull DialerCall call);
 
   /**
+   * Indicates there is a new incoming call that is about to be answered.
+   *
+   * @param call The call which is about to become active.
+   */
+  ListenableFuture<Void> onNewIncomingCall(@NonNull DialerCall call);
+
+  /**
    * Indicates the feature is available.
    *
    * @param context The application context.
diff --git a/java/com/android/incallui/speakeasy/SpeakEasyCallManagerStub.java b/java/com/android/incallui/speakeasy/SpeakEasyCallManagerStub.java
index a040973..da5e88a 100644
--- a/java/com/android/incallui/speakeasy/SpeakEasyCallManagerStub.java
+++ b/java/com/android/incallui/speakeasy/SpeakEasyCallManagerStub.java
@@ -22,6 +22,8 @@
 import android.support.v4.app.Fragment;
 import com.android.incallui.call.DialerCall;
 import com.google.common.base.Optional;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 import javax.inject.Inject;
 
 /** Default implementation of SpeakEasyCallManager. */
@@ -41,6 +43,11 @@
   @Override
   public void onCallRemoved(DialerCall call) {}
 
+  @Override
+  public ListenableFuture<Void> onNewIncomingCall(@NonNull DialerCall call) {
+    return Futures.immediateFuture(null);
+  }
+
   /** Always returns false. */
   @Override
   public boolean isAvailable(@NonNull Context unused) {
diff --git a/java/com/android/incallui/videotech/duo/DuoVideoTech.java b/java/com/android/incallui/videotech/duo/DuoVideoTech.java
index fdaed07..ac74e54 100644
--- a/java/com/android/incallui/videotech/duo/DuoVideoTech.java
+++ b/java/com/android/incallui/videotech/duo/DuoVideoTech.java
@@ -23,6 +23,7 @@
 import android.telecom.PhoneAccountHandle;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.concurrent.DefaultFutureCallback;
 import com.android.dialer.configprovider.ConfigProviderBindings;
 import com.android.dialer.duo.Duo;
 import com.android.dialer.duo.DuoListener;
@@ -33,6 +34,8 @@
 import com.android.incallui.videotech.utils.SessionModificationState;
 import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.MoreExecutors;
 
 public class DuoVideoTech implements VideoTech, DuoListener {
   private final Duo duo;
@@ -77,7 +80,10 @@
     if (!isRemoteUpgradeAvailabilityQueried) {
       LogUtil.v("DuoVideoTech.isAvailable", "reachability unknown, starting remote query");
       isRemoteUpgradeAvailabilityQueried = true;
-      duo.updateReachability(context, ImmutableList.of(callingNumber));
+      Futures.addCallback(
+          duo.updateReachability(context, ImmutableList.of(callingNumber)),
+          new DefaultFutureCallback<>(),
+          MoreExecutors.directExecutor());
     }
 
     return false;
diff --git a/java/com/android/voicemail/VoicemailComponent.java b/java/com/android/voicemail/VoicemailComponent.java
index bed75f0..0e09627 100644
--- a/java/com/android/voicemail/VoicemailComponent.java
+++ b/java/com/android/voicemail/VoicemailComponent.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import com.android.dialer.inject.HasRootComponent;
+import com.android.dialer.inject.IncludeInDialerRoot;
 import dagger.Subcomponent;
 
 /** Subcomponent that can be used to access the voicemail implementation. */
@@ -32,6 +33,7 @@
   }
 
   /** Used to refer to the root application component. */
+  @IncludeInDialerRoot
   public interface HasComponent {
     VoicemailComponent voicemailComponent();
   }
diff --git a/java/com/android/voicemail/stub/StubVoicemailModule.java b/java/com/android/voicemail/stub/StubVoicemailModule.java
index 6c1552c..efcb4cf 100644
--- a/java/com/android/voicemail/stub/StubVoicemailModule.java
+++ b/java/com/android/voicemail/stub/StubVoicemailModule.java
@@ -16,6 +16,8 @@
 
 package com.android.voicemail.stub;
 
+import com.android.dialer.inject.DialerVariant;
+import com.android.dialer.inject.InstallIn;
 import com.android.voicemail.VoicemailClient;
 import dagger.Binds;
 import dagger.Module;
@@ -24,6 +26,7 @@
 /**
  * A no-op version of the voicemail module for build targets that don't support the new OTMP client.
  */
+@InstallIn(variants = {DialerVariant.DIALER_TEST})
 @Module
 public abstract class StubVoicemailModule {