Merge "Use real prebuilt module for JavaPoet"
diff --git a/assets/quantum/res/drawable/quantum_ic_arrow_back_vd_theme_24.xml b/assets/quantum/res/drawable/quantum_ic_arrow_back_vd_theme_24.xml
index 3a85b7d..12db78d 100644
--- a/assets/quantum/res/drawable/quantum_ic_arrow_back_vd_theme_24.xml
+++ b/assets/quantum/res/drawable/quantum_ic_arrow_back_vd_theme_24.xml
@@ -18,7 +18,8 @@
         android:height="24dp"
         android:viewportWidth="24.0"
         android:viewportHeight="24.0"
-        android:tint="?attr/colorControlNormal">
+        android:tint="?attr/colorControlNormal"
+        android:autoMirrored="true">
     <path
         android:fillColor="@android:color/white"
         android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
diff --git a/java/com/android/bubble/Bubble.java b/java/com/android/bubble/Bubble.java
index e192e06..1b853cf 100644
--- a/java/com/android/bubble/Bubble.java
+++ b/java/com/android/bubble/Bubble.java
@@ -39,6 +39,9 @@
   /** Returns whether the bubble is currently visible */
   boolean isVisible();
 
+  /** Returns whether the bubble is currently dismissed */
+  boolean isDismissed();
+
   /**
    * Set the info for this Bubble to display
    *
diff --git a/java/com/android/bubble/stub/BubbleStub.java b/java/com/android/bubble/stub/BubbleStub.java
index 267f33f..2aa55a3 100644
--- a/java/com/android/bubble/stub/BubbleStub.java
+++ b/java/com/android/bubble/stub/BubbleStub.java
@@ -40,6 +40,11 @@
   }
 
   @Override
+  public boolean isDismissed() {
+    return false;
+  }
+
+  @Override
   public void setBubbleInfo(@NonNull BubbleInfo bubbleInfo) {}
 
   @Override
diff --git a/java/com/android/dialer/app/DialtactsActivity.java b/java/com/android/dialer/app/DialtactsActivity.java
index 9057cd9..c819fec 100644
--- a/java/com/android/dialer/app/DialtactsActivity.java
+++ b/java/com/android/dialer/app/DialtactsActivity.java
@@ -1130,7 +1130,7 @@
 
     NewSearchFragment fragment = (NewSearchFragment) getFragmentManager().findFragmentByTag(tag);
     if (fragment == null) {
-      fragment = NewSearchFragment.newInstance(!isDialpadShown());
+      fragment = NewSearchFragment.newInstance();
       transaction.add(R.id.dialtacts_frame, fragment, tag);
     } else {
       transaction.show(fragment);
diff --git a/java/com/android/dialer/app/calllog/CallLogNotificationsService.java b/java/com/android/dialer/app/calllog/CallLogNotificationsService.java
index 10e30ff..d84bd42 100644
--- a/java/com/android/dialer/app/calllog/CallLogNotificationsService.java
+++ b/java/com/android/dialer/app/calllog/CallLogNotificationsService.java
@@ -150,12 +150,12 @@
     LogUtil.i("CallLogNotificationsService.onHandleIntent", "action: " + action);
     switch (action) {
       case ACTION_MARK_ALL_NEW_VOICEMAILS_AS_OLD:
-        VoicemailQueryHandler.markAllNewVoicemailsAsRead(this);
+        VoicemailQueryHandler.markAllNewVoicemailsAsOld(this);
         VisualVoicemailNotifier.cancelAllVoicemailNotifications(this);
         break;
       case ACTION_MARK_SINGLE_NEW_VOICEMAIL_AS_OLD:
         Uri voicemailUri = intent.getData();
-        VoicemailQueryHandler.markSingleNewVoicemailAsRead(this, voicemailUri);
+        VoicemailQueryHandler.markSingleNewVoicemailAsOld(this, voicemailUri);
         VisualVoicemailNotifier.cancelSingleVoicemailNotification(this, voicemailUri);
         break;
       case ACTION_LEGACY_VOICEMAIL_DISMISSED:
diff --git a/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java b/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java
index 096488a..680424a 100644
--- a/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java
+++ b/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java
@@ -356,6 +356,9 @@
         new DialogInterface.OnClickListener() {
           @Override
           public void onClick(final DialogInterface dialog, final int button) {
+            VoicemailComponent.get(context)
+                .getVoicemailClient()
+                .setVoicemailDonationEnabled(context, details.accountHandle, false);
             dialog.cancel();
             recordPromoShown(context);
             ratingView.setVisibility(View.GONE);
diff --git a/java/com/android/dialer/app/calllog/VisualVoicemailUpdateTask.java b/java/com/android/dialer/app/calllog/VisualVoicemailUpdateTask.java
index b353b3a..bae30fa 100644
--- a/java/com/android/dialer/app/calllog/VisualVoicemailUpdateTask.java
+++ b/java/com/android/dialer/app/calllog/VisualVoicemailUpdateTask.java
@@ -212,6 +212,10 @@
             "found voicemail from spam number, suppressing notification");
         Logger.get(context)
             .logImpression(DialerImpression.Type.INCOMING_VOICEMAIL_AUTO_BLOCKED_AS_SPAM);
+        if (newCall.voicemailUri != null) {
+          // Mark auto blocked voicemail as old so that we don't process it again.
+          VoicemailQueryHandler.markSingleNewVoicemailAsOld(context, newCall.voicemailUri);
+        }
       } else {
         result.add(newCall);
       }
diff --git a/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java b/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java
index 169d0fd..5d8144c 100644
--- a/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java
+++ b/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java
@@ -42,7 +42,7 @@
   }
 
   @WorkerThread
-  public static void markAllNewVoicemailsAsRead(final @NonNull Context context) {
+  public static void markAllNewVoicemailsAsOld(final @NonNull Context context) {
     ThreadUtil.postOnUiThread(
         () -> {
           new VoicemailQueryHandler(context.getContentResolver())
@@ -51,10 +51,10 @@
   }
 
   @WorkerThread
-  public static void markSingleNewVoicemailAsRead(
+  public static void markSingleNewVoicemailAsOld(
       final @NonNull Context context, final Uri voicemailUri) {
     if (voicemailUri == null) {
-      LogUtil.e("VoicemailQueryHandler.markSingleNewVoicemailAsRead", "voicemail URI is null");
+      LogUtil.e("VoicemailQueryHandler.markSingleNewVoicemailAsOld", "voicemail URI is null");
       return;
     }
     ThreadUtil.postOnUiThread(
diff --git a/java/com/android/dialer/blocking/BlockNumberDialogFragment.java b/java/com/android/dialer/blocking/BlockNumberDialogFragment.java
index 621287f..de974cb 100644
--- a/java/com/android/dialer/blocking/BlockNumberDialogFragment.java
+++ b/java/com/android/dialer/blocking/BlockNumberDialogFragment.java
@@ -41,6 +41,7 @@
  * Fragment for confirming and enacting blocking/unblocking a number. Also invokes snackbar
  * providing undo functionality.
  */
+@Deprecated
 public class BlockNumberDialogFragment extends DialogFragment {
 
   private static final String BLOCK_DIALOG_FRAGMENT = "BlockNumberDialog";
diff --git a/java/com/android/dialer/blocking/BlockedNumbersAutoMigrator.java b/java/com/android/dialer/blocking/BlockedNumbersAutoMigrator.java
index 6e9fe13..8a57f29 100644
--- a/java/com/android/dialer/blocking/BlockedNumbersAutoMigrator.java
+++ b/java/com/android/dialer/blocking/BlockedNumbersAutoMigrator.java
@@ -33,6 +33,7 @@
  * android.provider.BlockedNumberContract} blocking. In order for this to happen, the user cannot
  * have any numbers that are blocked in the Dialer solution.
  */
+@Deprecated
 public class BlockedNumbersAutoMigrator {
 
   static final String HAS_CHECKED_AUTO_MIGRATE_KEY = "checkedAutoMigrate";
diff --git a/java/com/android/dialer/blocking/BlockedNumbersMigrator.java b/java/com/android/dialer/blocking/BlockedNumbersMigrator.java
index 61ebf2f..101a04b 100644
--- a/java/com/android/dialer/blocking/BlockedNumbersMigrator.java
+++ b/java/com/android/dialer/blocking/BlockedNumbersMigrator.java
@@ -36,6 +36,7 @@
  * {@link android.provider.BlockedNumberContract} blocking.
  */
 @TargetApi(VERSION_CODES.N)
+@Deprecated
 public class BlockedNumbersMigrator {
 
   private final Context context;
diff --git a/java/com/android/dialer/blocking/Blocking.java b/java/com/android/dialer/blocking/Blocking.java
new file mode 100644
index 0000000..e86d0a6
--- /dev/null
+++ b/java/com/android/dialer/blocking/Blocking.java
@@ -0,0 +1,117 @@
+/*
+ * 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.blocking;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.BlockedNumberContract.BlockedNumbers;
+import android.support.annotation.Nullable;
+import android.telephony.PhoneNumberUtils;
+import com.android.dialer.common.database.Selection;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+
+/** Blocks and unblocks number. */
+public final class Blocking {
+
+  private Blocking() {}
+
+  /**
+   * Thrown when blocking cannot be performed because dialer is not the default dialer, or the
+   * current user is not a primary user.
+   *
+   * <p>Blocking is only allowed on the primary user (the first user added). Primary user cannot be
+   * easily checked because {@link
+   * android.provider.BlockedNumberContract#canCurrentUserBlockNumbers(Context)} is a slow IPC, and
+   * UserManager.isPrimaryUser() is a system API. Since secondary users are rare cases this class
+   * choose to ignore the check and let callers handle the failure later.
+   */
+  public static final class BlockingFailedException extends Exception {
+    BlockingFailedException(Throwable cause) {
+      super(cause);
+    }
+  }
+
+  /**
+   * Block a number.
+   *
+   * @param countryIso the current location used to guess the country code of the number if not
+   *     available. If {@code null} and {@code number} does not have a country code, only the
+   *     original number will be blocked.
+   * @throws BlockingFailedException in the returned future if the operation failed.
+   */
+  public static ListenableFuture<Void> block(
+      Context context,
+      ListeningExecutorService executorService,
+      String number,
+      @Nullable String countryIso) {
+    return executorService.submit(
+        () -> {
+          ContentValues values = new ContentValues();
+          values.put(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, number);
+          String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso);
+          if (e164Number != null) {
+            values.put(BlockedNumbers.COLUMN_E164_NUMBER, e164Number);
+          }
+          try {
+            context.getContentResolver().insert(BlockedNumbers.CONTENT_URI, values);
+          } catch (SecurityException e) {
+            throw new BlockingFailedException(e);
+          }
+          return null;
+        });
+  }
+
+  /**
+   * Unblock a number.
+   *
+   * @param countryIso the current location used to guess the country code of the number if not
+   *     available. If {@code null} and {@code number} does not have a country code, only the
+   *     original number will be unblocked.
+   * @throws BlockingFailedException in the returned future if the operation failed.
+   */
+  public static ListenableFuture<Void> unblock(
+      Context context,
+      ListeningExecutorService executorService,
+      String number,
+      @Nullable String countryIso) {
+    return executorService.submit(
+        () -> {
+          Selection selection =
+              Selection.column(BlockedNumbers.COLUMN_ORIGINAL_NUMBER).is("=", number);
+          String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso);
+          if (e164Number != null) {
+            selection =
+                selection
+                    .buildUpon()
+                    .or(Selection.column(BlockedNumbers.COLUMN_E164_NUMBER).is("=", e164Number))
+                    .build();
+          }
+          try {
+            context
+                .getContentResolver()
+                .delete(
+                    BlockedNumbers.CONTENT_URI,
+                    selection.getSelection(),
+                    selection.getSelectionArgs());
+          } catch (SecurityException e) {
+            throw new BlockingFailedException(e);
+          }
+          return null;
+        });
+  }
+}
diff --git a/java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java b/java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java
index 8be479c..b417592 100644
--- a/java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java
+++ b/java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java
@@ -38,6 +38,7 @@
 import java.util.concurrent.ConcurrentHashMap;
 
 /** TODO(calderwoodra): documentation */
+@Deprecated
 public class FilteredNumberAsyncQueryHandler extends AsyncQueryHandler {
 
   public static final int INVALID_ID = -1;
diff --git a/java/com/android/dialer/blocking/FilteredNumberCompat.java b/java/com/android/dialer/blocking/FilteredNumberCompat.java
index b0af45c..d263d21 100644
--- a/java/com/android/dialer/blocking/FilteredNumberCompat.java
+++ b/java/com/android/dialer/blocking/FilteredNumberCompat.java
@@ -48,6 +48,7 @@
  * referencing columns from either contract class in situations where both blocking solutions may be
  * used.
  */
+@Deprecated
 public class FilteredNumberCompat {
 
   private static Boolean canAttemptBlockOperationsForTest;
diff --git a/java/com/android/dialer/blocking/FilteredNumberProvider.java b/java/com/android/dialer/blocking/FilteredNumberProvider.java
index 3fad4e2..547892b 100644
--- a/java/com/android/dialer/blocking/FilteredNumberProvider.java
+++ b/java/com/android/dialer/blocking/FilteredNumberProvider.java
@@ -34,6 +34,7 @@
 import com.android.dialer.location.GeoUtil;
 
 /** Filtered number content provider. */
+@Deprecated
 public class FilteredNumberProvider extends ContentProvider {
 
   private static final int FILTERED_NUMBERS_TABLE = 1;
diff --git a/java/com/android/dialer/blocking/FilteredNumbersUtil.java b/java/com/android/dialer/blocking/FilteredNumbersUtil.java
index 6433355..d839ef5 100644
--- a/java/com/android/dialer/blocking/FilteredNumbersUtil.java
+++ b/java/com/android/dialer/blocking/FilteredNumbersUtil.java
@@ -42,6 +42,7 @@
 import java.util.concurrent.TimeUnit;
 
 /** Utility to help with tasks related to filtered numbers. */
+@Deprecated
 public class FilteredNumbersUtil {
 
   public static final String CALL_BLOCKING_NOTIFICATION_TAG = "call_blocking";
diff --git a/java/com/android/dialer/blocking/MigrateBlockedNumbersDialogFragment.java b/java/com/android/dialer/blocking/MigrateBlockedNumbersDialogFragment.java
index 9b416ff..9a3b647 100644
--- a/java/com/android/dialer/blocking/MigrateBlockedNumbersDialogFragment.java
+++ b/java/com/android/dialer/blocking/MigrateBlockedNumbersDialogFragment.java
@@ -30,6 +30,7 @@
  * Dialog fragment shown to users when they need to migrate to use {@link
  * android.provider.BlockedNumberContract} for blocking.
  */
+@Deprecated
 public class MigrateBlockedNumbersDialogFragment extends DialogFragment {
 
   private BlockedNumbersMigrator blockedNumbersMigrator;
diff --git a/java/com/android/dialer/blockreportspam/ShowBlockReportSpamDialogReceiver.java b/java/com/android/dialer/blockreportspam/ShowBlockReportSpamDialogReceiver.java
index cc307b6..fd26ab5 100644
--- a/java/com/android/dialer/blockreportspam/ShowBlockReportSpamDialogReceiver.java
+++ b/java/com/android/dialer/blockreportspam/ShowBlockReportSpamDialogReceiver.java
@@ -21,8 +21,9 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.support.annotation.Nullable;
-import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
+import android.widget.Toast;
+import com.android.dialer.blocking.Blocking;
+import com.android.dialer.blocking.Blocking.BlockingFailedException;
 import com.android.dialer.blockreportspam.BlockReportSpamDialogs.DialogFragmentForBlockingNumber;
 import com.android.dialer.blockreportspam.BlockReportSpamDialogs.DialogFragmentForBlockingNumberAndOptionallyReportingAsSpam;
 import com.android.dialer.blockreportspam.BlockReportSpamDialogs.DialogFragmentForReportingNotSpam;
@@ -31,15 +32,16 @@
 import com.android.dialer.blockreportspam.BlockReportSpamDialogs.OnSpamDialogClickListener;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
-import com.android.dialer.common.concurrent.DialerExecutor.Worker;
 import com.android.dialer.common.concurrent.DialerExecutorComponent;
 import com.android.dialer.logging.DialerImpression;
+import com.android.dialer.logging.DialerImpression.Type;
 import com.android.dialer.logging.Logger;
 import com.android.dialer.protos.ProtoParsers;
 import com.android.dialer.spam.Spam;
 import com.android.dialer.spam.SpamComponent;
 import com.android.dialer.spam.SpamSettings;
-import com.google.auto.value.AutoValue;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
 
 /**
  * A {@link BroadcastReceiver} that shows an appropriate dialog upon receiving notifications from
@@ -106,8 +108,6 @@
 
     Spam spam = SpamComponent.get(context).spam();
     SpamSettings spamSettings = SpamComponent.get(context).spamSettings();
-    FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler =
-        new FilteredNumberAsyncQueryHandler(context);
 
     // Set up the positive listener for the dialog.
     OnSpamDialogClickListener onSpamDialogClickListener =
@@ -132,12 +132,7 @@
                 dialogInfo.getContactSource());
           }
 
-          filteredNumberAsyncQueryHandler.blockNumber(
-              unused ->
-                  Logger.get(context)
-                      .logImpression(DialerImpression.Type.USER_ACTION_BLOCKED_NUMBER),
-              dialogInfo.getNormalizedNumber(),
-              dialogInfo.getCountryIso());
+          blockNumber(context, dialogInfo);
         };
 
     // Create and show the dialog.
@@ -157,19 +152,11 @@
         ProtoParsers.getTrusted(
             intent, EXTRA_DIALOG_INFO, BlockReportSpamDialogInfo.getDefaultInstance());
 
-    FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler =
-        new FilteredNumberAsyncQueryHandler(context);
-
     // Set up the positive listener for the dialog.
     OnConfirmListener onConfirmListener =
         () -> {
           LogUtil.i("ShowBlockReportSpamDialogReceiver.showDialogToBlockNumber", "block number");
-          filteredNumberAsyncQueryHandler.blockNumber(
-              unused ->
-                  Logger.get(context)
-                      .logImpression(DialerImpression.Type.USER_ACTION_BLOCKED_NUMBER),
-              dialogInfo.getNormalizedNumber(),
-              dialogInfo.getCountryIso());
+          blockNumber(context, dialogInfo);
         };
 
     // Create and show the dialog.
@@ -219,46 +206,12 @@
         ProtoParsers.getTrusted(
             intent, EXTRA_DIALOG_INFO, BlockReportSpamDialogInfo.getDefaultInstance());
 
-    FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler =
-        new FilteredNumberAsyncQueryHandler(context);
-
     // Set up the positive listener for the dialog.
     OnConfirmListener onConfirmListener =
         () -> {
           LogUtil.i("ShowBlockReportSpamDialogReceiver.showDialogToUnblockNumber", "confirmed");
 
-          DialerExecutorComponent.get(context)
-              .dialerExecutorFactory()
-              .createNonUiTaskBuilder(
-                  new GetIdForBlockedNumberWorker(filteredNumberAsyncQueryHandler))
-              .onSuccess(
-                  idForBlockedNumber -> {
-                    LogUtil.i(
-                        "ShowBlockReportSpamDialogReceiver.showDialogToUnblockNumber",
-                        "ID for the blocked number retrieved");
-                    if (idForBlockedNumber == null) {
-                      throw new IllegalStateException("ID for a blocked number is null.");
-                    }
-
-                    LogUtil.i(
-                        "ShowBlockReportSpamDialogReceiver.showDialogToUnblockNumber",
-                        "unblocking number");
-                    filteredNumberAsyncQueryHandler.unblock(
-                        (rows, values) ->
-                            Logger.get(context)
-                                .logImpression(DialerImpression.Type.USER_ACTION_UNBLOCKED_NUMBER),
-                        idForBlockedNumber);
-                  })
-              .onFailure(
-                  throwable -> {
-                    throw new RuntimeException(throwable);
-                  })
-              .build()
-              .executeSerial(
-                  NumberInfo.newBuilder()
-                      .setNormalizedNumber(dialogInfo.getNormalizedNumber())
-                      .setCountryIso(dialogInfo.getCountryIso())
-                      .build());
+          unblockNumber(context, dialogInfo);
         };
 
     // Create & show the dialog.
@@ -267,46 +220,58 @@
         .show(fragmentManager, BlockReportSpamDialogs.UNBLOCK_DIALOG_TAG);
   }
 
-  /** A {@link Worker} that retrieves the ID of a blocked number from the database. */
-  private static final class GetIdForBlockedNumberWorker implements Worker<NumberInfo, Integer> {
+  private static void blockNumber(Context context, BlockReportSpamDialogInfo dialogInfo) {
+    Logger.get(context).logImpression(Type.USER_ACTION_BLOCKED_NUMBER);
+    Futures.addCallback(
+        Blocking.block(
+            context,
+            DialerExecutorComponent.get(context).backgroundExecutor(),
+            dialogInfo.getNormalizedNumber(),
+            dialogInfo.getCountryIso()),
+        new FutureCallback<Void>() {
+          @Override
+          public void onSuccess(Void unused) {
+            // Do nothing
+          }
 
-    private final FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler;
-
-    GetIdForBlockedNumberWorker(FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler) {
-      this.filteredNumberAsyncQueryHandler = filteredNumberAsyncQueryHandler;
-    }
-
-    @Nullable
-    @Override
-    public Integer doInBackground(NumberInfo input) throws Throwable {
-      LogUtil.enterBlock("GetIdForBlockedNumberWorker.doInBackground");
-
-      return filteredNumberAsyncQueryHandler.getBlockedIdSynchronous(
-          input.getNormalizedNumber(), input.getCountryIso());
-    }
+          @Override
+          public void onFailure(Throwable throwable) {
+            if (throwable instanceof BlockingFailedException) {
+              Logger.get(context).logImpression(Type.USER_ACTION_BLOCK_NUMBER_FAILED);
+              Toast.makeText(context, R.string.block_number_failed_toast, Toast.LENGTH_LONG).show();
+            } else {
+              throw new RuntimeException(throwable);
+            }
+          }
+        },
+        DialerExecutorComponent.get(context).uiExecutor());
   }
 
-  /**
-   * Contains information about a number and serves as the input to {@link
-   * GetIdForBlockedNumberWorker}.
-   */
-  @AutoValue
-  abstract static class NumberInfo {
-    static Builder newBuilder() {
-      return new AutoValue_ShowBlockReportSpamDialogReceiver_NumberInfo.Builder();
-    }
+  private static void unblockNumber(Context context, BlockReportSpamDialogInfo dialogInfo) {
+    Logger.get(context).logImpression(Type.USER_ACTION_UNBLOCKED_NUMBER);
+    Futures.addCallback(
+        Blocking.unblock(
+            context,
+            DialerExecutorComponent.get(context).backgroundExecutor(),
+            dialogInfo.getNormalizedNumber(),
+            dialogInfo.getCountryIso()),
+        new FutureCallback<Void>() {
+          @Override
+          public void onSuccess(Void unused) {
+            // Do nothing
+          }
 
-    abstract String getNormalizedNumber();
-
-    abstract String getCountryIso();
-
-    @AutoValue.Builder
-    abstract static class Builder {
-      abstract Builder setNormalizedNumber(String normalizedNumber);
-
-      abstract Builder setCountryIso(String countryIso);
-
-      abstract NumberInfo build();
-    }
+          @Override
+          public void onFailure(Throwable throwable) {
+            if (throwable instanceof BlockingFailedException) {
+              Logger.get(context).logImpression(Type.USER_ACTION_UNBLOCK_NUMBER_FAILED);
+              Toast.makeText(context, R.string.unblock_number_failed_toast, Toast.LENGTH_LONG)
+                  .show();
+            } else {
+              throw new RuntimeException(throwable);
+            }
+          }
+        },
+        DialerExecutorComponent.get(context).uiExecutor());
   }
 }
diff --git a/java/com/android/dialer/blockreportspam/res/values/strings.xml b/java/com/android/dialer/blockreportspam/res/values/strings.xml
index aface92..1537c23 100644
--- a/java/com/android/dialer/blockreportspam/res/values/strings.xml
+++ b/java/com/android/dialer/blockreportspam/res/values/strings.xml
@@ -60,4 +60,9 @@
   <!-- Label for checkbox in the Alert dialog to allow the user to report the number as spam as well.  [CHAR LIMIT=30] -->
   <string name="checkbox_report_as_spam_action">Report call as spam</string>
 
+  <!-- Toast if the user clicked the block button but it failed. [CHAR LIMIT=100] -->
+  <string name="block_number_failed_toast">Problem blocking this number</string>
+
+  <!-- Toast if the user clicked the unblock button but it failed. [CHAR LIMIT=100] -->
+  <string name="unblock_number_failed_toast">Problem unblocking this number</string>
 </resources>
diff --git a/java/com/android/dialer/calllog/ClearMissedCalls.java b/java/com/android/dialer/calllog/ClearMissedCalls.java
index d216e7b..78eb802 100644
--- a/java/com/android/dialer/calllog/ClearMissedCalls.java
+++ b/java/com/android/dialer/calllog/ClearMissedCalls.java
@@ -38,8 +38,8 @@
 import javax.inject.Inject;
 
 /**
- * Clears missed calls. This includes cancelling notifications and updating the "NEW" status in the
- * system call log.
+ * Clears missed calls. This includes cancelling notifications and updating the "IS_READ" status in
+ * the system call log.
  */
 public final class ClearMissedCalls {
 
@@ -58,11 +58,11 @@
   }
 
   /**
-   * Cancels all missed call notifications and marks all "new" missed calls in the system call log
-   * as "not new".
+   * Cancels all missed call notifications and marks all "unread" missed calls in the system call
+   * log as "read".
    */
   public ListenableFuture<Void> clearAll() {
-    ListenableFuture<Void> markNewFuture = markNotNew(ImmutableSet.of());
+    ListenableFuture<Void> markReadFuture = markRead(ImmutableSet.of());
     ListenableFuture<Void> cancelNotificationsFuture =
         uiThreadExecutor.submit(
             () -> {
@@ -73,11 +73,11 @@
     // Note on this usage of whenAllComplete:
     //   -The returned future completes when all sub-futures complete (whether they fail or not)
     //   -The returned future fails if any sub-future fails
-    return Futures.whenAllComplete(markNewFuture, cancelNotificationsFuture)
+    return Futures.whenAllComplete(markReadFuture, cancelNotificationsFuture)
         .call(
             () -> {
               // Calling get() is necessary to propagate failures.
-              markNewFuture.get();
+              markReadFuture.get();
               cancelNotificationsFuture.get();
               return null;
             },
@@ -86,12 +86,12 @@
 
   /**
    * For the provided set of IDs from the system call log, cancels their missed call notifications
-   * and marks them "not new".
+   * and marks them "read".
    *
    * @param ids IDs from the system call log (see {@link Calls#_ID}}.
    */
   public ListenableFuture<Void> clearBySystemCallLogId(Collection<Long> ids) {
-    ListenableFuture<Void> markNewFuture = markNotNew(ids);
+    ListenableFuture<Void> markReadFuture = markRead(ids);
     ListenableFuture<Void> cancelNotificationsFuture =
         uiThreadExecutor.submit(
             () -> {
@@ -105,11 +105,11 @@
     // Note on this usage of whenAllComplete:
     //   -The returned future completes when all sub-futures complete (whether they fail or not)
     //   -The returned future fails if any sub-future fails
-    return Futures.whenAllComplete(markNewFuture, cancelNotificationsFuture)
+    return Futures.whenAllComplete(markReadFuture, cancelNotificationsFuture)
         .call(
             () -> {
               // Calling get() is necessary to propagate failures.
-              markNewFuture.get();
+              markReadFuture.get();
               cancelNotificationsFuture.get();
               return null;
             },
@@ -117,28 +117,28 @@
   }
 
   /**
-   * Marks all provided system call log IDs as not new, or if the provided collection is empty,
-   * marks all calls as not new.
+   * Marks all provided system call log IDs as read, or if the provided collection is empty, marks
+   * all calls as read.
    */
   @SuppressLint("MissingPermission")
-  private ListenableFuture<Void> markNotNew(Collection<Long> ids) {
+  private ListenableFuture<Void> markRead(Collection<Long> ids) {
     return backgroundExecutor.submit(
         () -> {
           if (!UserManagerCompat.isUserUnlocked(appContext)) {
-            LogUtil.e("ClearMissedCalls.markNotNew", "locked");
+            LogUtil.e("ClearMissedCalls.markRead", "locked");
             return null;
           }
           if (!PermissionsUtil.hasCallLogWritePermissions(appContext)) {
-            LogUtil.e("ClearMissedCalls.markNotNew", "no permission");
+            LogUtil.e("ClearMissedCalls.markRead", "no permission");
             return null;
           }
 
           ContentValues values = new ContentValues();
-          values.put(Calls.NEW, 0);
+          values.put(Calls.IS_READ, 1);
 
           Selection.Builder selectionBuilder =
               Selection.builder()
-                  .and(Selection.column(Calls.NEW).is("=", 1))
+                  .and(Selection.column(Calls.IS_READ).is("=", 0))
                   .and(Selection.column(Calls.TYPE).is("=", Calls.MISSED_TYPE));
           if (!ids.isEmpty()) {
             selectionBuilder.and(Selection.column(Calls._ID).in(toStrings(ids)));
diff --git a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java
index 8362a81..aa4260c 100644
--- a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java
+++ b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java
@@ -225,6 +225,7 @@
     return new RowCombiner(individualRowsSortedByTimestampDesc)
         .useMostRecentLong(AnnotatedCallLog.TIMESTAMP)
         .useMostRecentLong(AnnotatedCallLog.NEW)
+        .useMostRecentLong(AnnotatedCallLog.IS_READ)
         // Two different DialerPhoneNumbers could be combined if they are different but considered
         // to be an "exact match" by libphonenumber; in this case we arbitrarily select the most
         // recent one.
diff --git a/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java b/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java
index 05a3399..839ba33 100644
--- a/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java
+++ b/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java
@@ -25,6 +25,7 @@
 import android.view.ViewGroup;
 import com.android.dialer.calllogutils.CallLogDates;
 import com.android.dialer.common.Assert;
+import com.android.dialer.logging.Logger;
 import com.android.dialer.time.Clock;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -53,6 +54,7 @@
 
   private final Clock clock;
   private final RealtimeRowProcessor realtimeRowProcessor;
+  private final PopCounts popCounts = new PopCounts();
 
   private Cursor cursor;
 
@@ -76,6 +78,7 @@
   void updateCursor(Cursor updatedCursor) {
     this.cursor = updatedCursor;
     this.realtimeRowProcessor.clearCache();
+    this.popCounts.reset();
 
     setHeaderPositions();
     notifyDataSetChanged();
@@ -85,6 +88,10 @@
     this.realtimeRowProcessor.clearCache();
   }
 
+  void logMetrics(Context context) {
+    Logger.get(context).logAnnotatedCallLogMetrics(popCounts.popped, popCounts.didNotPop);
+  }
+
   private void setHeaderPositions() {
     // If there are no rows to display, set all header positions to null.
     if (!cursor.moveToFirst()) {
@@ -138,7 +145,8 @@
             LayoutInflater.from(viewGroup.getContext())
                 .inflate(R.layout.new_call_log_entry, viewGroup, false),
             clock,
-            realtimeRowProcessor);
+            realtimeRowProcessor,
+            popCounts);
       default:
         throw Assert.createUnsupportedOperationFailException("Unsupported view type: " + viewType);
     }
@@ -207,4 +215,14 @@
     }
     return cursor.getCount() + numberOfHeaders;
   }
+
+  static class PopCounts {
+    int popped;
+    int didNotPop;
+
+    private void reset() {
+      popped = 0;
+      didNotPop = 0;
+    }
+  }
 }
diff --git a/java/com/android/dialer/calllog/ui/NewCallLogFragment.java b/java/com/android/dialer/calllog/ui/NewCallLogFragment.java
index bb1a730..0f1c251 100644
--- a/java/com/android/dialer/calllog/ui/NewCallLogFragment.java
+++ b/java/com/android/dialer/calllog/ui/NewCallLogFragment.java
@@ -89,6 +89,15 @@
   }
 
   @Override
+  public void onStop() {
+    super.onStop();
+
+    if (recyclerView.getAdapter() != null) {
+      ((NewCallLogAdapter) recyclerView.getAdapter()).logMetrics(getContext());
+    }
+  }
+
+  @Override
   public void onPause() {
     super.onPause();
     LogUtil.enterBlock("NewCallLogFragment.onPause");
diff --git a/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java b/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java
index f322b56..217208d 100644
--- a/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java
+++ b/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java
@@ -27,6 +27,7 @@
 import android.widget.ImageView;
 import android.widget.TextView;
 import com.android.dialer.calllog.model.CoalescedRow;
+import com.android.dialer.calllog.ui.NewCallLogAdapter.PopCounts;
 import com.android.dialer.calllog.ui.menu.NewCallLogMenu;
 import com.android.dialer.calllogutils.CallLogEntryText;
 import com.android.dialer.calllogutils.CallLogIntents;
@@ -60,10 +61,12 @@
   private final Clock clock;
   private final RealtimeRowProcessor realtimeRowProcessor;
   private final ExecutorService uiExecutorService;
+  private final PopCounts popCounts;
 
   private long currentRowId;
 
-  NewCallLogViewHolder(View view, Clock clock, RealtimeRowProcessor realtimeRowProcessor) {
+  NewCallLogViewHolder(
+      View view, Clock clock, RealtimeRowProcessor realtimeRowProcessor, PopCounts popCounts) {
     super(view);
     this.context = view.getContext();
     contactPhotoView = view.findViewById(R.id.contact_photo_view);
@@ -79,6 +82,7 @@
 
     this.clock = clock;
     this.realtimeRowProcessor = realtimeRowProcessor;
+    this.popCounts = popCounts;
     uiExecutorService = DialerExecutorComponent.get(context).uiExecutor();
   }
 
@@ -105,11 +109,11 @@
     primaryTextView.setText(CallLogEntryText.buildPrimaryText(context, row));
     secondaryTextView.setText(CallLogEntryText.buildSecondaryTextForEntries(context, clock, row));
 
-    if (isNewMissedCall(row)) {
-      primaryTextView.setTextAppearance(R.style.primary_textview_new_call);
-      callCountTextView.setTextAppearance(R.style.primary_textview_new_call);
-      secondaryTextView.setTextAppearance(R.style.secondary_textview_new_call);
-      phoneAccountView.setTextAppearance(R.style.phoneaccount_textview_new_call);
+    if (isUnreadMissedCall(row)) {
+      primaryTextView.setTextAppearance(R.style.primary_textview_unread_call);
+      callCountTextView.setTextAppearance(R.style.primary_textview_unread_call);
+      secondaryTextView.setTextAppearance(R.style.secondary_textview_unread_call);
+      phoneAccountView.setTextAppearance(R.style.phoneaccount_textview_unread_call);
     } else {
       primaryTextView.setTextAppearance(R.style.primary_textview);
       callCountTextView.setTextAppearance(R.style.primary_textview);
@@ -136,10 +140,11 @@
     }
   }
 
-  private boolean isNewMissedCall(CoalescedRow row) {
+  private boolean isUnreadMissedCall(CoalescedRow row) {
     // Show missed call styling if the most recent call in the group was missed and it is still
-    // marked as NEW. It is not clear what IS_READ should be used for and it is currently not used.
-    return row.getCallType() == Calls.MISSED_TYPE && row.getIsNew();
+    // marked as not read. The "NEW" column is presumably used for notifications and voicemails
+    // only.
+    return row.getCallType() == Calls.MISSED_TYPE && !row.getIsRead();
   }
 
   private void setPhoto(CoalescedRow row) {
@@ -155,7 +160,7 @@
     ColorStateList colorStateList =
         ColorStateList.valueOf(
             context.getColor(
-                isNewMissedCall(row)
+                isUnreadMissedCall(row)
                     ? R.color.feature_icon_unread_color
                     : R.color.feature_icon_read_color));
 
@@ -213,7 +218,7 @@
     }
     callTypeIcon.setImageResource(resId);
 
-    if (isNewMissedCall(row)) {
+    if (isUnreadMissedCall(row)) {
       callTypeIcon.setImageTintList(
           ColorStateList.valueOf(context.getColor(R.color.call_type_icon_unread_color)));
     } else {
@@ -258,13 +263,17 @@
       // If the user scrolled then this ViewHolder may not correspond to the completed task and
       // there's nothing to do.
       if (originalRow.getId() != currentRowId) {
+        popCounts.didNotPop++;
         return;
       }
       // Only update the UI if the updated row differs from the original row (which has already
       // been displayed).
       if (!updatedRow.equals(originalRow)) {
         displayRow(updatedRow);
+        popCounts.popped++;
+        return;
       }
+      popCounts.didNotPop++;
     }
 
     @Override
diff --git a/java/com/android/dialer/calllog/ui/RealtimeRowProcessor.java b/java/com/android/dialer/calllog/ui/RealtimeRowProcessor.java
index b955e02..c5148d9 100644
--- a/java/com/android/dialer/calllog/ui/RealtimeRowProcessor.java
+++ b/java/com/android/dialer/calllog/ui/RealtimeRowProcessor.java
@@ -198,8 +198,14 @@
 
   private CoalescedRow applyPhoneLookupInfoToRow(
       PhoneLookupInfo phoneLookupInfo, CoalescedRow row) {
+    // Force the "cp2_info_incomplete" value to the original value so that it is not used when
+    // comparing the original row to the updated row.
+    // TODO(linyuh): Improve the comparison instead.
     return row.toBuilder()
-        .setNumberAttributes(NumberAttributesConverter.fromPhoneLookupInfo(phoneLookupInfo).build())
+        .setNumberAttributes(
+            NumberAttributesConverter.fromPhoneLookupInfo(phoneLookupInfo)
+                .setIsCp2InfoIncomplete(row.getNumberAttributes().getIsCp2InfoIncomplete())
+                .build())
         .build();
   }
 }
diff --git a/java/com/android/dialer/calllog/ui/menu/NewCallLogMenu.java b/java/com/android/dialer/calllog/ui/menu/NewCallLogMenu.java
index dabb9bb..3869e78 100644
--- a/java/com/android/dialer/calllog/ui/menu/NewCallLogMenu.java
+++ b/java/com/android/dialer/calllog/ui/menu/NewCallLogMenu.java
@@ -35,9 +35,9 @@
       HistoryItemActionBottomSheet.show(
           context, BottomSheetHeader.fromRow(context, row), Modules.fromRow(context, row));
 
-      // If the user opens the bottom sheet for a new call, clear the notifications and make the row
-      // not bold immediately. To do this, mark all of the calls in group as not new.
-      if (row.getIsNew() && row.getCallType() == Calls.MISSED_TYPE) {
+      // If the user opens the bottom sheet for an unread call, clear the notifications and make the
+      // row not bold immediately. To do this, mark all of the calls in group as read.
+      if (!row.getIsRead() && row.getCallType() == Calls.MISSED_TYPE) {
         Futures.addCallback(
             CallLogComponent.get(context)
                 .getClearMissedCalls()
diff --git a/java/com/android/dialer/calllog/ui/res/values/styles.xml b/java/com/android/dialer/calllog/ui/res/values/styles.xml
index d521fee..047f1da 100644
--- a/java/com/android/dialer/calllog/ui/res/values/styles.xml
+++ b/java/com/android/dialer/calllog/ui/res/values/styles.xml
@@ -21,7 +21,7 @@
     <item name="android:fontFamily">sans-serif</item>
   </style>
 
-  <style name="primary_textview_new_call">
+  <style name="primary_textview_unread_call">
     <item name="android:textColor">@color/primary_text_color</item>
     <item name="android:fontFamily">sans-serif-medium</item>
   </style>
@@ -35,12 +35,12 @@
     <item name="android:fontFamily">sans-serif</item>
   </style>
 
-  <style name="secondary_textview_new_call">
+  <style name="secondary_textview_unread_call">
     <item name="android:textColor">@color/missed_call</item>
     <item name="android:fontFamily">sans-serif-medium</item>
   </style>
 
-  <style name="phoneaccount_textview_new_call">
+  <style name="phoneaccount_textview_unread_call">
     <item name="android:fontFamily">sans-serif-medium</item>
   </style>
 
diff --git a/java/com/android/dialer/calllogutils/CallLogEntryText.java b/java/com/android/dialer/calllogutils/CallLogEntryText.java
index c778691..e346de0 100644
--- a/java/com/android/dialer/calllogutils/CallLogEntryText.java
+++ b/java/com/android/dialer/calllogutils/CallLogEntryText.java
@@ -20,6 +20,7 @@
 import android.provider.CallLog.Calls;
 import android.text.TextUtils;
 import com.android.dialer.calllog.model.CoalescedRow;
+import com.android.dialer.duo.DuoConstants;
 import com.android.dialer.time.Clock;
 import com.google.common.base.Optional;
 import com.google.common.collect.Collections2;
@@ -72,20 +73,22 @@
    * <p>Rules:
    *
    * <ul>
-   *   <li>For numbers that are not spam or blocked: (Duo video, )?$Label|$Location • Date
-   *   <li>For blocked non-spam numbers: Blocked • (Duo video, )?$Label|$Location • Date
-   *   <li>For spam but not blocked numbers: Spam • (Duo video, )?$Label • Date
-   *   <li>For blocked spam numbers: Blocked • Spam • (Duo video, )?$Label • 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 •
+   *       Date
+   *   <li>For spam but not blocked numbers: Spam • $Label(, Duo video|Carrier video)? • Date
+   *   <li>For blocked spam numbers: Blocked • Spam • $Label(, Duo video|Carrier video)? • Date
    * </ul>
    *
    * <p>Examples:
    *
    * <ul>
-   *   <li>Duo Video, Mobile • Now
-   *   <li>Duo Video • 10 min ago
+   *   <li>Mobile, Duo video • Now
+   *   <li>Duo video • 10 min ago
    *   <li>Mobile • 11:45 PM
    *   <li>Mobile • Sun
-   *   <li>Blocked • Duo Video, Mobile • Now
+   *   <li>Blocked • Mobile, Duo video • Now
    *   <li>Blocked • Brooklyn, NJ • 10 min ago
    *   <li>Spam • Mobile • Now
    *   <li>Spam • Now
@@ -125,20 +128,20 @@
     /*
      * Rules:
      *   For numbers that are not spam or blocked:
-     *     (Duo video, )?$Label|$Location [• NumberIfNoName]?
+     *     $Label(, Duo video|Carrier video)?|$Location [• NumberIfNoName]?
      *   For blocked non-spam numbers:
-     *     Blocked • (Duo video, )?$Label|$Location [• NumberIfNoName]?
+     *     Blocked • $Label(, Duo video|Carrier video)?|$Location [• NumberIfNoName]?
      *   For spam but not blocked numbers:
-     *     Spam • (Duo video, )?$Label [• NumberIfNoName]?
+     *     Spam • $Label(, Duo video|Carrier video)? [• NumberIfNoName]?
      *   For blocked spam numbers:
-     *     Blocked • Spam • (Duo video, )?$Label [• NumberIfNoName]?
+     *     Blocked • Spam • $Label(, Duo video|Carrier video)? [• NumberIfNoName]?
      *
      * The number is shown at the end if there is no name for the entry. (It is shown in primary
      * text otherwise.)
      *
      * Examples:
-     *   Duo Video, Mobile • 555-1234
-     *   Duo Video • 555-1234
+     *   Mobile, Duo video • 555-1234
+     *   Duo video • 555-1234
      *   Mobile • 555-1234
      *   Blocked • Mobile • 555-1234
      *   Blocked • Brooklyn, NJ • 555-1234
@@ -178,7 +181,7 @@
   }
 
   /**
-   * Returns a value such as "Duo Video, Mobile" without the time of the call or formatted number
+   * Returns a value such as "Mobile, Duo video" without the time of the call or formatted number
    * appended.
    *
    * <p>When the secondary text is shown in call log entry list, this prefix is suffixed with the
@@ -187,18 +190,30 @@
    */
   private static CharSequence getNumberTypeLabel(Context context, CoalescedRow row) {
     StringBuilder secondaryText = new StringBuilder();
-    if ((row.getFeatures() & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO) {
-      // TODO(zachh): Add "Duo" prefix?
-      secondaryText.append(context.getText(R.string.new_call_log_video));
-    }
+
+    // The number type label comes first (e.g., "Mobile", "Work", "Home", etc).
     String numberTypeLabel = row.getNumberAttributes().getNumberTypeLabel();
-    if (!TextUtils.isEmpty(numberTypeLabel)) {
+    secondaryText.append(numberTypeLabel);
+
+    // Add video call info if applicable.
+    if ((row.getFeatures() & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO) {
       if (secondaryText.length() > 0) {
         secondaryText.append(", ");
       }
-      secondaryText.append(numberTypeLabel);
-    } else if (!row.getNumberAttributes().getIsSpam()) {
-      // Don't show the location if there's a number type label or the number is spam.
+
+      boolean isDuoCall =
+          DuoConstants.PHONE_ACCOUNT_COMPONENT_NAME
+              .flattenToString()
+              .equals(row.getPhoneAccountComponentName());
+      secondaryText.append(
+          context.getText(
+              isDuoCall ? R.string.new_call_log_duo_video : R.string.new_call_log_carrier_video));
+    }
+
+    // Show the location if
+    // (1) there is no number type label, and
+    // (2) the number is not spam.
+    if (TextUtils.isEmpty(numberTypeLabel) && !row.getNumberAttributes().getIsSpam()) {
       String location = row.getGeocodedLocation();
       if (!TextUtils.isEmpty(location)) {
         if (secondaryText.length() > 0) {
@@ -207,6 +222,7 @@
         secondaryText.append(location);
       }
     }
+
     return secondaryText;
   }
 
diff --git a/java/com/android/dialer/calllogutils/res/values/strings.xml b/java/com/android/dialer/calllogutils/res/values/strings.xml
index f536ca6..bc19ce2 100644
--- a/java/com/android/dialer/calllogutils/res/values/strings.xml
+++ b/java/com/android/dialer/calllogutils/res/values/strings.xml
@@ -131,8 +131,11 @@
   <!-- String to be displayed to indicate in the call log that a call just now occurred. -->
   <string name="just_now">Just now</string>
 
-  <!-- Text to show in call log for a video call. [CHAR LIMIT=16] -->
-  <string name="new_call_log_video">Video</string>
+  <!-- Text to show in call log for a carrier video call. [CHAR LIMIT=30] -->
+  <string name="new_call_log_carrier_video">Carrier video</string>
+
+  <!-- Text to show in call log for a duo video call. [CHAR LIMIT=30] -->
+  <string name="new_call_log_duo_video">Duo video</string>
 
   <!-- String used to display calls from unknown numbers in the call log.  [CHAR LIMIT=30] -->
   <string name="new_call_log_unknown">Unknown</string>
diff --git a/java/com/android/dialer/commandline/CommandLineModule.java b/java/com/android/dialer/commandline/CommandLineModule.java
index 6121556..9155787 100644
--- a/java/com/android/dialer/commandline/CommandLineModule.java
+++ b/java/com/android/dialer/commandline/CommandLineModule.java
@@ -16,7 +16,7 @@
 
 package com.android.dialer.commandline;
 
-import com.android.dialer.commandline.impl.Blocking;
+import com.android.dialer.commandline.impl.BlockingCommand;
 import com.android.dialer.commandline.impl.CallCommand;
 import com.android.dialer.commandline.impl.Echo;
 import com.android.dialer.commandline.impl.Help;
@@ -43,16 +43,20 @@
     private final Help help;
     private final Version version;
     private final Echo echo;
-    private final Blocking blocking;
+    private final BlockingCommand blockingCommand;
     private final CallCommand callCommand;
 
     @Inject
     AospCommandInjector(
-        Help help, Version version, Echo echo, Blocking blocking, CallCommand callCommand) {
+        Help help,
+        Version version,
+        Echo echo,
+        BlockingCommand blockingCommand,
+        CallCommand callCommand) {
       this.help = help;
       this.version = version;
       this.echo = echo;
-      this.blocking = blocking;
+      this.blockingCommand = blockingCommand;
       this.callCommand = callCommand;
     }
 
@@ -60,7 +64,7 @@
       builder.addCommand("help", help);
       builder.addCommand("version", version);
       builder.addCommand("echo", echo);
-      builder.addCommand("blocking", blocking);
+      builder.addCommand("blocking", blockingCommand);
       builder.addCommand("call", callCommand);
       return builder;
     }
diff --git a/java/com/android/dialer/commandline/impl/Blocking.java b/java/com/android/dialer/commandline/impl/Blocking.java
deleted file mode 100644
index 2afd165..0000000
--- a/java/com/android/dialer/commandline/impl/Blocking.java
+++ /dev/null
@@ -1,96 +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.commandline.impl;
-
-import android.content.Context;
-import android.support.annotation.NonNull;
-import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
-import com.android.dialer.commandline.Arguments;
-import com.android.dialer.commandline.Command;
-import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor;
-import com.android.dialer.inject.ApplicationContext;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.ListeningExecutorService;
-import javax.inject.Inject;
-
-/** Block or unblock a number. */
-public class Blocking implements Command {
-
-  @NonNull
-  @Override
-  public String getShortDescription() {
-    return "block or unblock numbers";
-  }
-
-  @NonNull
-  @Override
-  public String getUsage() {
-    return "blocking block|unblock|isblocked number\n\n" + "number should be e.164 formatted";
-  }
-
-  private final Context appContext;
-  private final ListeningExecutorService executorService;
-
-  @Inject
-  Blocking(
-      @ApplicationContext Context context,
-      @BackgroundExecutor ListeningExecutorService executorService) {
-    this.appContext = context;
-    this.executorService = executorService;
-  }
-
-  @Override
-  public ListenableFuture<String> run(Arguments args) throws IllegalCommandLineArgumentException {
-    // AsyncQueryHandler must be created on a thread with looper.
-    // TODO(a bug): Use blocking version
-    FilteredNumberAsyncQueryHandler asyncQueryHandler =
-        new FilteredNumberAsyncQueryHandler(appContext);
-    return executorService.submit(() -> doInBackground(args, asyncQueryHandler));
-  }
-
-  private String doInBackground(Arguments args, FilteredNumberAsyncQueryHandler asyncQueryHandler) {
-    if (args.getPositionals().isEmpty()) {
-      return getUsage();
-    }
-
-    String command = args.getPositionals().get(0);
-
-    if ("block".equals(command)) {
-      String number = args.getPositionals().get(1);
-      asyncQueryHandler.blockNumber((unused) -> {}, number, null);
-      return "blocked " + number;
-    }
-
-    if ("unblock".equals(command)) {
-      String number = args.getPositionals().get(1);
-      Integer id = asyncQueryHandler.getBlockedIdSynchronous(number, null);
-      if (id == null) {
-        return number + " is not blocked";
-      }
-      asyncQueryHandler.unblock((unusedRows, unusedValues) -> {}, id);
-      return "unblocked " + number;
-    }
-
-    if ("isblocked".equals(command)) {
-      String number = args.getPositionals().get(1);
-      Integer id = asyncQueryHandler.getBlockedIdSynchronous(number, null);
-      return id == null ? "false" : "true";
-    }
-
-    return getUsage();
-  }
-}
diff --git a/java/com/android/dialer/commandline/impl/BlockingCommand.java b/java/com/android/dialer/commandline/impl/BlockingCommand.java
new file mode 100644
index 0000000..c8f8934
--- /dev/null
+++ b/java/com/android/dialer/commandline/impl/BlockingCommand.java
@@ -0,0 +1,111 @@
+/*
+ * 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.commandline.impl;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import com.android.dialer.DialerPhoneNumber;
+import com.android.dialer.blocking.Blocking;
+import com.android.dialer.commandline.Arguments;
+import com.android.dialer.commandline.Command;
+import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor;
+import com.android.dialer.inject.ApplicationContext;
+import com.android.dialer.phonelookup.PhoneLookupComponent;
+import com.android.dialer.phonelookup.PhoneLookupInfo;
+import com.android.dialer.phonelookup.consolidator.PhoneLookupInfoConsolidator;
+import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.i18n.phonenumbers.PhoneNumberUtil;
+import javax.inject.Inject;
+
+/** Block or unblock a number. */
+public class BlockingCommand implements Command {
+
+  @NonNull
+  @Override
+  public String getShortDescription() {
+    return "block or unblock numbers";
+  }
+
+  @NonNull
+  @Override
+  public String getUsage() {
+    return "blocking block|unblock|isblocked number\n\n" + "number should be e.164 formatted";
+  }
+
+  private final Context appContext;
+  private final ListeningExecutorService executorService;
+
+  @Inject
+  BlockingCommand(
+      @ApplicationContext Context context,
+      @BackgroundExecutor ListeningExecutorService executorService) {
+    this.appContext = context;
+    this.executorService = executorService;
+  }
+
+  @Override
+  public ListenableFuture<String> run(Arguments args) throws IllegalCommandLineArgumentException {
+    if (args.getPositionals().isEmpty()) {
+      return Futures.immediateFuture(getUsage());
+    }
+
+    String command = args.getPositionals().get(0);
+
+    if ("block".equals(command)) {
+      String number = args.getPositionals().get(1);
+      return Futures.transform(
+          Blocking.block(appContext, executorService, number, null),
+          (unused) -> "blocked " + number,
+          MoreExecutors.directExecutor());
+    }
+
+    if ("unblock".equals(command)) {
+      String number = args.getPositionals().get(1);
+      return Futures.transform(
+          Blocking.unblock(appContext, executorService, number, null),
+          (unused) -> "unblocked " + number,
+          MoreExecutors.directExecutor());
+    }
+
+    if ("isblocked".equals(command)) {
+      String number = args.getPositionals().get(1);
+      ListenableFuture<DialerPhoneNumber> dialerPhoneNumberFuture =
+          executorService.submit(
+              () -> new DialerPhoneNumberUtil(PhoneNumberUtil.getInstance()).parse(number, null));
+
+      ListenableFuture<PhoneLookupInfo> lookupFuture =
+          Futures.transformAsync(
+              dialerPhoneNumberFuture,
+              (dialerPhoneNumber) ->
+                  PhoneLookupComponent.get(appContext)
+                      .compositePhoneLookup()
+                      .lookup(dialerPhoneNumber),
+              executorService);
+
+      return Futures.transform(
+          lookupFuture,
+          (info) -> new PhoneLookupInfoConsolidator(info).isBlocked() ? "true" : "false",
+          MoreExecutors.directExecutor());
+    }
+
+    return Futures.immediateFuture(getUsage());
+  }
+}
diff --git a/java/com/android/dialer/logging/LoggingBindings.java b/java/com/android/dialer/logging/LoggingBindings.java
index a6795ed..7c580cb 100644
--- a/java/com/android/dialer/logging/LoggingBindings.java
+++ b/java/com/android/dialer/logging/LoggingBindings.java
@@ -90,4 +90,10 @@
 
   /** Logs a call auto-blocked in call screening. */
   void logAutoBlockedCall(String phoneNumber);
+
+  /** Logs annotated call log metrics. */
+  void logAnnotatedCallLogMetrics(int invalidNumbersInCallLog);
+
+  /** Logs annotated call log metrics. */
+  void logAnnotatedCallLogMetrics(int numberRowsThatDidPop, int numberRowsThatDidNotPop);
 }
diff --git a/java/com/android/dialer/logging/LoggingBindingsStub.java b/java/com/android/dialer/logging/LoggingBindingsStub.java
index de08f44..65ebd1a 100644
--- a/java/com/android/dialer/logging/LoggingBindingsStub.java
+++ b/java/com/android/dialer/logging/LoggingBindingsStub.java
@@ -64,4 +64,10 @@
 
   @Override
   public void logAutoBlockedCall(String phoneNumber) {}
+
+  @Override
+  public void logAnnotatedCallLogMetrics(int invalidNumbersInCallLog) {}
+
+  @Override
+  public void logAnnotatedCallLogMetrics(int numberRowsThatDidPop, int numberRowsThatDidNotPop) {}
 }
diff --git a/java/com/android/dialer/logging/dialer_impression.proto b/java/com/android/dialer/logging/dialer_impression.proto
index 16efd13..96a7bb6 100644
--- a/java/com/android/dialer/logging/dialer_impression.proto
+++ b/java/com/android/dialer/logging/dialer_impression.proto
@@ -12,7 +12,7 @@
   // Event enums to be used for Impression Logging in Dialer.
   // It's perfectly acceptable for this enum to be large
   // Values should be from 1000 to 100000.
-  // Next Tag: 1369
+  // Next Tag: 1371
   enum Type {
     UNKNOWN_AOSP_EVENT_TYPE = 1000;
 
@@ -78,9 +78,17 @@
 
         ;
 
+    // User made it to the last step but blocking failed because user is
+    // secondary or dialer is not default
+    USER_ACTION_BLOCK_NUMBER_FAILED = 1369;
+
     // User made it to the last step and actually unblocked the number
     USER_ACTION_UNBLOCKED_NUMBER = 1013;
 
+    // User made it to the last step but unblocking failed because user is
+    // secondary or dialer is not default
+    USER_ACTION_UNBLOCK_NUMBER_FAILED = 1370;
+
     // User blocked a number, does not guarantee if the number was reported as
     // spam or not To compute the number of blocked numbers that were reported
     // as not spam and yet blocked Subtract this value from
diff --git a/java/com/android/dialer/main/impl/MainSearchController.java b/java/com/android/dialer/main/impl/MainSearchController.java
index c2ff051..7b4bc35 100644
--- a/java/com/android/dialer/main/impl/MainSearchController.java
+++ b/java/com/android/dialer/main/impl/MainSearchController.java
@@ -149,25 +149,24 @@
 
     // Show Search
     if (searchFragment == null) {
-      // TODO(a bug): zero suggest results aren't actually shown but this enabled the nearby
-      // places promo to be shown.
-      searchFragment = NewSearchFragment.newInstance(/* showZeroSuggest=*/ true);
+      searchFragment = NewSearchFragment.newInstance();
       transaction.add(R.id.search_fragment_container, searchFragment, SEARCH_FRAGMENT_TAG);
       transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
     } else if (!isSearchVisible()) {
       transaction.show(searchFragment);
     }
-    searchFragment.setQuery("", CallInitiationType.Type.DIALPAD);
 
     // Show Dialpad
     if (getDialpadFragment() == null) {
       DialpadFragment dialpadFragment = new DialpadFragment();
       dialpadFragment.setStartedFromNewIntent(fromNewIntent);
       transaction.add(R.id.dialpad_fragment_container, dialpadFragment, DIALPAD_FRAGMENT_TAG);
+      searchFragment.setQuery("", CallInitiationType.Type.DIALPAD);
     } else {
       DialpadFragment dialpadFragment = getDialpadFragment();
       dialpadFragment.setStartedFromNewIntent(fromNewIntent);
       transaction.show(dialpadFragment);
+      searchFragment.setQuery(dialpadFragment.getQuery(), CallInitiationType.Type.DIALPAD);
     }
     transaction.commit();
 
@@ -258,7 +257,7 @@
       } else {
         Logger.get(activity)
             .logImpression(DialerImpression.Type.MAIN_TOUCH_SEARCH_LIST_TO_HIDE_KEYBOARD);
-        toolbar.hideKeyboard();
+        closeKeyboard();
       }
     }
   }
@@ -348,6 +347,14 @@
     return isSearchVisible();
   }
 
+  /** Closes the keyboard if necessary. */
+  private void closeKeyboard() {
+    NewSearchFragment fragment = getSearchFragment();
+    if (fragment != null && fragment.isAdded()) {
+      toolbar.hideKeyboard();
+    }
+  }
+
   /**
    * Opens search in regular/search bar search mode.
    *
@@ -376,9 +383,7 @@
 
     // Show Search
     if (searchFragment == null) {
-      // TODO(a bug): zero suggest results aren't actually shown but this enabled the nearby
-      // places promo to be shown.
-      searchFragment = NewSearchFragment.newInstance(true);
+      searchFragment = NewSearchFragment.newInstance();
       transaction.add(R.id.search_fragment_container, searchFragment, SEARCH_FRAGMENT_TAG);
       transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
     } else if (!isSearchVisible()) {
@@ -446,6 +451,9 @@
 
   @Override
   public void onActivityPause() {
+    LogUtil.enterBlock("MainSearchController.onActivityPause");
+    closeKeyboard();
+
     if (closeSearchOnPause) {
       closeSearchOnPause = false;
       if (isInSearch()) {
@@ -462,7 +470,7 @@
       closeSearchOnPause = !requestingPermission;
 
       // Always hide the keyboard when the user leaves dialer (including permission requests)
-      toolbar.hideKeyboard();
+      closeKeyboard();
     }
   }
 
@@ -473,6 +481,7 @@
 
   @Override
   public void requestingPermission() {
+    LogUtil.enterBlock("MainSearchController.requestingPermission");
     requestingPermission = true;
   }
 
diff --git a/java/com/android/dialer/phonelookup/cp2/Cp2DefaultDirectoryPhoneLookup.java b/java/com/android/dialer/phonelookup/cp2/Cp2DefaultDirectoryPhoneLookup.java
index 902a2fb..c5d4e53 100644
--- a/java/com/android/dialer/phonelookup/cp2/Cp2DefaultDirectoryPhoneLookup.java
+++ b/java/com/android/dialer/phonelookup/cp2/Cp2DefaultDirectoryPhoneLookup.java
@@ -33,7 +33,9 @@
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor;
 import com.android.dialer.common.concurrent.Annotations.LightweightExecutor;
+import com.android.dialer.configprovider.ConfigProvider;
 import com.android.dialer.inject.ApplicationContext;
+import com.android.dialer.logging.Logger;
 import com.android.dialer.phonelookup.PhoneLookup;
 import com.android.dialer.phonelookup.PhoneLookupInfo;
 import com.android.dialer.phonelookup.PhoneLookupInfo.Cp2Info;
@@ -64,15 +66,11 @@
   private static final String PREF_LAST_TIMESTAMP_PROCESSED =
       "cp2DefaultDirectoryPhoneLookupLastTimestampProcessed";
 
-  // We cannot efficiently process invalid numbers because batch queries cannot be constructed which
-  // accomplish the necessary loose matching. We'll attempt to process a limited number of them,
-  // but if there are too many we fall back to querying CP2 at render time.
-  private static final int MAX_SUPPORTED_INVALID_NUMBERS = 5;
-
   private final Context appContext;
   private final SharedPreferences sharedPreferences;
   private final ListeningExecutorService backgroundExecutorService;
   private final ListeningExecutorService lightweightExecutorService;
+  private final ConfigProvider configProvider;
 
   @Nullable private Long currentLastTimestampProcessed;
 
@@ -81,11 +79,13 @@
       @ApplicationContext Context appContext,
       @Unencrypted SharedPreferences sharedPreferences,
       @BackgroundExecutor ListeningExecutorService backgroundExecutorService,
-      @LightweightExecutor ListeningExecutorService lightweightExecutorService) {
+      @LightweightExecutor ListeningExecutorService lightweightExecutorService,
+      ConfigProvider configProvider) {
     this.appContext = appContext;
     this.sharedPreferences = sharedPreferences;
     this.backgroundExecutorService = backgroundExecutorService;
     this.lightweightExecutorService = lightweightExecutorService;
+    this.configProvider = configProvider;
   }
 
   @Override
@@ -138,7 +138,7 @@
   @Override
   public ListenableFuture<Boolean> isDirty(ImmutableSet<DialerPhoneNumber> phoneNumbers) {
     PartitionedNumbers partitionedNumbers = new PartitionedNumbers(phoneNumbers);
-    if (partitionedNumbers.invalidNumbers().size() > MAX_SUPPORTED_INVALID_NUMBERS) {
+    if (partitionedNumbers.invalidNumbers().size() > getMaxSupportedInvalidNumbers()) {
       // If there are N invalid numbers, we can't determine determine dirtiness without running N
       // queries; since running this many queries is not feasible for the (lightweight) isDirty
       // check, simply return true. The expectation is that this should rarely be the case as the
@@ -234,7 +234,8 @@
 
     // Then run a separate query for each invalid number. Separate queries are done to accomplish
     // loose matching which couldn't be accomplished with a batch query.
-    Assert.checkState(partitionedNumbers.invalidNumbers().size() <= MAX_SUPPORTED_INVALID_NUMBERS);
+    Assert.checkState(
+        partitionedNumbers.invalidNumbers().size() <= getMaxSupportedInvalidNumbers());
     for (String invalidNumber : partitionedNumbers.invalidNumbers()) {
       queryFutures.add(queryPhoneLookupTableForContactIdsBasedOnRawNumber(invalidNumber));
     }
@@ -529,7 +530,11 @@
       ImmutableMap<DialerPhoneNumber, Cp2Info> existingInfoMap) {
     ArraySet<DialerPhoneNumber> unprocessableNumbers = new ArraySet<>();
     PartitionedNumbers partitionedNumbers = new PartitionedNumbers(existingInfoMap.keySet());
-    if (partitionedNumbers.invalidNumbers().size() > MAX_SUPPORTED_INVALID_NUMBERS) {
+
+    int invalidNumberCount = partitionedNumbers.invalidNumbers().size();
+    Logger.get(appContext).logAnnotatedCallLogMetrics(invalidNumberCount);
+
+    if (invalidNumberCount > getMaxSupportedInvalidNumbers()) {
       for (String invalidNumber : partitionedNumbers.invalidNumbers()) {
         unprocessableNumbers.addAll(partitionedNumbers.dialerPhoneNumbersForInvalid(invalidNumber));
       }
@@ -928,4 +933,13 @@
     }
     return where.toString();
   }
+
+  /**
+   * We cannot efficiently process invalid numbers because batch queries cannot be constructed which
+   * accomplish the necessary loose matching. We'll attempt to process a limited number of them, but
+   * if there are too many we fall back to querying CP2 at render time.
+   */
+  private long getMaxSupportedInvalidNumbers() {
+    return configProvider.getLong("cp2_phone_lookup_max_invalid_numbers", 5);
+  }
 }
diff --git a/java/com/android/dialer/searchfragment/list/NewSearchFragment.java b/java/com/android/dialer/searchfragment/list/NewSearchFragment.java
index 505f1c6..51befe8 100644
--- a/java/com/android/dialer/searchfragment/list/NewSearchFragment.java
+++ b/java/com/android/dialer/searchfragment/list/NewSearchFragment.java
@@ -67,7 +67,6 @@
 import com.android.dialer.searchfragment.directories.DirectoryContactsCursorLoader;
 import com.android.dialer.searchfragment.list.SearchActionViewHolder.Action;
 import com.android.dialer.searchfragment.nearbyplaces.NearbyPlacesCursorLoader;
-import com.android.dialer.storage.StorageComponent;
 import com.android.dialer.util.CallUtil;
 import com.android.dialer.util.DialerUtils;
 import com.android.dialer.util.PermissionsUtil;
@@ -94,7 +93,6 @@
   // updates so they are bundled together
   private static final int ENRICHED_CALLING_CAPABILITIES_UPDATED_DELAY = 400;
 
-  private static final String KEY_SHOW_ZERO_SUGGEST = "use_zero_suggest";
   private static final String KEY_LOCATION_PROMPT_DISMISSED = "search_location_prompt_dismissed";
 
   @VisibleForTesting public static final int READ_CONTACTS_PERMISSION_REQUEST_CODE = 1;
@@ -134,12 +132,8 @@
 
   private Runnable updatePositionRunnable;
 
-  public static NewSearchFragment newInstance(boolean showZeroSuggest) {
-    NewSearchFragment fragment = new NewSearchFragment();
-    Bundle args = new Bundle();
-    args.putBoolean(KEY_SHOW_ZERO_SUGGEST, showZeroSuggest);
-    fragment.setArguments(args);
-    return fragment;
+  public static NewSearchFragment newInstance() {
+    return new NewSearchFragment();
   }
 
   @Nullable
@@ -150,7 +144,7 @@
     adapter = new SearchAdapter(getContext(), new SearchCursorManager(), this);
     adapter.setQuery(query, rawNumber);
     adapter.setSearchActions(getActions());
-    adapter.setZeroSuggestVisible(getArguments().getBoolean(KEY_SHOW_ZERO_SUGGEST));
+    showLocationPermission();
     emptyContentView = view.findViewById(R.id.empty_view);
     recyclerView = view.findViewById(R.id.recycler_view);
     recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
@@ -265,13 +259,31 @@
     if (adapter != null) {
       adapter.setQuery(query, rawNumber);
       adapter.setSearchActions(getActions());
-      adapter.setZeroSuggestVisible(isRegularSearch());
+      showLocationPermission();
       loadCp2ContactsCursor();
       loadNearbyPlacesCursor();
       loadDirectoryContactsCursors();
     }
   }
 
+  /** Returns true if the location permission was shown. */
+  private boolean showLocationPermission() {
+    if (adapter == null) {
+      return false;
+    }
+
+    if (PermissionsUtil.hasLocationPermissions(getContext())
+        || hasBeenDismissed()
+        || !isRegularSearch()) {
+      adapter.hideLocationPermissionRequest();
+      return false;
+    }
+
+    adapter.showLocationPermissionRequest(
+        v -> requestLocationPermission(), v -> dismissLocationPermission());
+    return true;
+  }
+
   /** Translate the search fragment and resize it to fit on the screen. */
   public void animatePosition(int start, int end, int duration) {
     // Called before the view is ready, prepare a runnable to run in onCreateView
@@ -382,16 +394,16 @@
    * <p>Should not be called before finishing loading info about all directories (local and remote).
    */
   private void loadNearbyPlacesCursor() {
-    if (!PermissionsUtil.hasLocationPermissions(getContext())
-        && !StorageComponent.get(getContext())
-            .unencryptedSharedPrefs()
-            .getBoolean(KEY_LOCATION_PROMPT_DISMISSED, false)) {
-      if (adapter != null && isRegularSearch() && !hasBeenDismissed()) {
-        adapter.showLocationPermissionRequest(
-            v -> requestLocationPermission(), v -> dismissLocationPermission());
-      }
+    // If we're requesting the location permission, don't load nearby places cursor.
+    if (showLocationPermission()) {
       return;
     }
+
+    // If the user dismissed the prompt without granting us the permission, don't load the cursor.
+    if (!PermissionsUtil.hasLocationPermissions(getContext())) {
+      return;
+    }
+
     // Cancel existing load if one exists.
     ThreadUtil.getUiThreadHandler().removeCallbacks(loadNearbyPlacesRunnable);
 
diff --git a/java/com/android/dialer/searchfragment/list/SearchAdapter.java b/java/com/android/dialer/searchfragment/list/SearchAdapter.java
index 805eaf5..74b60c6 100644
--- a/java/com/android/dialer/searchfragment/list/SearchAdapter.java
+++ b/java/com/android/dialer/searchfragment/list/SearchAdapter.java
@@ -42,7 +42,6 @@
   private final SearchCursorManager searchCursorManager;
   private final Context context;
 
-  private boolean showZeroSuggest;
   private String query;
   // Raw query number from dialpad, which may contain special character such as "+". This is used
   // for actions to add contact or send sms.
@@ -138,21 +137,9 @@
 
   @Override
   public int getItemCount() {
-    if (TextUtils.isEmpty(query) && !showZeroSuggest) {
-      return 0;
-    }
     return searchCursorManager.getCount();
   }
 
-  /**
-   * @param visible If true and query is empty, the adapter won't show any list elements.
-   * @see #setQuery(String, String)
-   * @see #getItemCount()
-   */
-  public void setZeroSuggestVisible(boolean visible) {
-    showZeroSuggest = visible;
-  }
-
   public void setQuery(String query, @Nullable String rawNumber) {
     this.query = query;
     this.rawNumber = rawNumber;
diff --git a/java/com/android/dialer/searchfragment/list/res/layout/search_action_layout.xml b/java/com/android/dialer/searchfragment/list/res/layout/search_action_layout.xml
index 99d0fbf..8b366fe 100644
--- a/java/com/android/dialer/searchfragment/list/res/layout/search_action_layout.xml
+++ b/java/com/android/dialer/searchfragment/list/res/layout/search_action_layout.xml
@@ -24,7 +24,7 @@
       android:layout_width="48dp"
       android:layout_height="48dp"
       android:layout_marginStart="8dp"
-      android:layout_gravity="center_vertical"
+      android:layout_gravity="center_vertical|start"
       android:padding="12dp"
       android:tint="@color/dialer_theme_color"/>
 
diff --git a/java/com/android/dialer/speeddial/database/SpeedDialEntry.java b/java/com/android/dialer/speeddial/database/SpeedDialEntry.java
new file mode 100644
index 0000000..aa90909
--- /dev/null
+++ b/java/com/android/dialer/speeddial/database/SpeedDialEntry.java
@@ -0,0 +1,116 @@
+/*
+ * 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.speeddial.database;
+
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
+import com.google.auto.value.AutoValue;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** POJO representation of database rows returned by {@link SpeedDialEntryDao}. */
+@AutoValue
+public abstract class SpeedDialEntry {
+
+  /** Unique ID */
+  public abstract long id();
+
+  /** @see {@link Contacts#_ID} */
+  public abstract long contactId();
+
+  /** @see {@link Contacts#LOOKUP_KEY} */
+  public abstract String lookupKey();
+
+  /**
+   * {@link Channel} that is associated with this entry.
+   *
+   * <p>Contacts with multiple channels do not have a default until specified by the user. Once the
+   * default channel is determined, all calls should be placed to this channel.
+   */
+  @Nullable
+  public abstract Channel defaultChannel();
+
+  public abstract Builder toBuilder();
+
+  public static Builder builder() {
+    return new AutoValue_SpeedDialEntry.Builder();
+  }
+
+  /** Builder class for speed dial entry. */
+  @AutoValue.Builder
+  public abstract static class Builder {
+
+    public abstract Builder setId(long id);
+
+    public abstract Builder setContactId(long contactId);
+
+    public abstract Builder setLookupKey(String lookupKey);
+
+    public abstract Builder setDefaultChannel(@Nullable Channel defaultChannel);
+
+    public abstract SpeedDialEntry build();
+  }
+
+  /** POJO representation of a relevant phone number columns in {@link SpeedDialEntryDao}. */
+  @AutoValue
+  public abstract static class Channel {
+
+    public static final int UNKNOWN = 0;
+    public static final int VOICE = 1;
+    public static final int VIDEO = 2;
+
+    /** Whether the Channel is for an audio or video call. */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({UNKNOWN, VOICE, VIDEO})
+    public @interface Technology {}
+
+    /**
+     * Raw phone number as the user entered it.
+     *
+     * @see {@link Phone#NUMBER}
+     */
+    public abstract String number();
+
+    /**
+     * Label that the user associated with this number like {@link Phone#TYPE_WORK}, {@link
+     * Phone#TYPE_HOME}, ect.
+     *
+     * @see {@link Phone#LABEL}
+     */
+    public abstract String label();
+
+    public abstract @Technology int technology();
+
+    public static Builder builder() {
+      return new AutoValue_SpeedDialEntry_Channel.Builder();
+    }
+
+    /** Builder class for {@link Channel}. */
+    @AutoValue.Builder
+    public abstract static class Builder {
+
+      public abstract Builder setNumber(String number);
+
+      public abstract Builder setLabel(String label);
+
+      public abstract Builder setTechnology(@Technology int technology);
+
+      public abstract Channel build();
+    }
+  }
+}
diff --git a/java/com/android/dialer/speeddial/database/SpeedDialEntryDao.java b/java/com/android/dialer/speeddial/database/SpeedDialEntryDao.java
new file mode 100644
index 0000000..39cb115
--- /dev/null
+++ b/java/com/android/dialer/speeddial/database/SpeedDialEntryDao.java
@@ -0,0 +1,57 @@
+/*
+ * 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.speeddial.database;
+
+import java.util.List;
+
+/** Interface that databases support speed dial entries should implement. */
+public interface SpeedDialEntryDao {
+
+  /** Return all entries in the database */
+  List<SpeedDialEntry> getAllEntries();
+
+  /**
+   * Insert new entries.
+   *
+   * <p>Fails if any of the {@link SpeedDialEntry#id()} already exist.
+   */
+  void insert(List<SpeedDialEntry> entries);
+
+  /**
+   * Insert a new entry.
+   *
+   * <p>Fails if the {@link SpeedDialEntry#id()} already exists.
+   */
+  long insert(SpeedDialEntry entry);
+
+  /**
+   * Updates existing entries based on {@link SpeedDialEntry#id}.
+   *
+   * <p>Fails if the {@link SpeedDialEntry#id()} doesn't exist.
+   */
+  void update(List<SpeedDialEntry> entries);
+
+  /**
+   * Delete the passed in entries based on {@link SpeedDialEntry#id}.
+   *
+   * <p>Fails if the {@link SpeedDialEntry#id()} doesn't exist.
+   */
+  void delete(List<Long> entries);
+
+  /** Delete all entries in the database. */
+  void deleteAll();
+}
diff --git a/java/com/android/dialer/speeddial/database/SpeedDialEntryDatabaseHelper.java b/java/com/android/dialer/speeddial/database/SpeedDialEntryDatabaseHelper.java
new file mode 100644
index 0000000..1812dbd
--- /dev/null
+++ b/java/com/android/dialer/speeddial/database/SpeedDialEntryDatabaseHelper.java
@@ -0,0 +1,221 @@
+/*
+ * 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.speeddial.database;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.text.TextUtils;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.database.Selection;
+import com.android.dialer.speeddial.database.SpeedDialEntry.Channel;
+import java.util.ArrayList;
+import java.util.List;
+
+/** {@link SpeedDialEntryDao} implemented as an SQLite database. */
+public final class SpeedDialEntryDatabaseHelper extends SQLiteOpenHelper
+    implements SpeedDialEntryDao {
+
+  private static final int DATABASE_VERSION = 1;
+  private static final String DATABASE_NAME = "CPSpeedDialEntry";
+
+  // Column names
+  private static final String TABLE_NAME = "speed_dial_entries";
+  private static final String ID = "id";
+  private static final String CONTACT_ID = "contact_id";
+  private static final String LOOKUP_KEY = "lookup_key";
+  private static final String PHONE_NUMBER = "phone_number";
+  private static final String PHONE_LABEL = "phone_label";
+  private static final String PHONE_TYPE = "phone_type";
+
+  // Column positions
+  private static final int POSITION_ID = 0;
+  private static final int POSITION_CONTACT_ID = 1;
+  private static final int POSITION_LOOKUP_KEY = 2;
+  private static final int POSITION_PHONE_NUMBER = 3;
+  private static final int POSITION_PHONE_LABEL = 4;
+  private static final int POSITION_PHONE_TYPE = 5;
+
+  // Create Table Query
+  private static final String CREATE_TABLE_SQL =
+      "create table if not exists "
+          + TABLE_NAME
+          + " ("
+          + (ID + " integer primary key, ")
+          + (CONTACT_ID + " integer, ")
+          + (LOOKUP_KEY + " text, ")
+          + (PHONE_NUMBER + " text, ")
+          + (PHONE_LABEL + " text, ")
+          + (PHONE_TYPE + " integer ")
+          + ");";
+
+  private static final String DELETE_TABLE_SQL = "drop table if exists " + TABLE_NAME;
+
+  public SpeedDialEntryDatabaseHelper(Context context) {
+    super(context, DATABASE_NAME, null, DATABASE_VERSION);
+  }
+
+  @Override
+  public void onCreate(SQLiteDatabase db) {
+    db.execSQL(CREATE_TABLE_SQL);
+  }
+
+  @Override
+  public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+    // TODO(calderwoodra): handle upgrades more elegantly
+    db.execSQL(DELETE_TABLE_SQL);
+    this.onCreate(db);
+  }
+
+  @Override
+  public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+    // TODO(calderwoodra): handle upgrades more elegantly
+    this.onUpgrade(db, oldVersion, newVersion);
+  }
+
+  @Override
+  public List<SpeedDialEntry> getAllEntries() {
+    List<SpeedDialEntry> entries = new ArrayList<>();
+
+    String query = "SELECT * FROM " + TABLE_NAME;
+    try (SQLiteDatabase db = getReadableDatabase();
+        Cursor cursor = db.rawQuery(query, null)) {
+      cursor.moveToPosition(-1);
+      while (cursor.moveToNext()) {
+        Channel channel =
+            Channel.builder()
+                .setNumber(cursor.getString(POSITION_PHONE_NUMBER))
+                .setLabel(cursor.getString(POSITION_PHONE_LABEL))
+                .setTechnology(cursor.getInt(POSITION_PHONE_TYPE))
+                .build();
+        if (TextUtils.isEmpty(channel.number())) {
+          channel = null;
+        }
+        SpeedDialEntry entry =
+            SpeedDialEntry.builder()
+                .setDefaultChannel(channel)
+                .setContactId(cursor.getLong(POSITION_CONTACT_ID))
+                .setLookupKey(cursor.getString(POSITION_LOOKUP_KEY))
+                .setId(cursor.getInt(POSITION_ID))
+                .build();
+        entries.add(entry);
+      }
+    }
+    return entries;
+  }
+
+  @Override
+  public void insert(List<SpeedDialEntry> entries) {
+    SQLiteDatabase db = getWritableDatabase();
+    db.beginTransaction();
+    try {
+      for (SpeedDialEntry entry : entries) {
+        if (db.insert(TABLE_NAME, null, buildContentValues(entry)) == -1L) {
+          throw Assert.createUnsupportedOperationFailException(
+              "Attempted to insert a row that already exists.");
+        }
+      }
+      db.setTransactionSuccessful();
+    } finally {
+      db.endTransaction();
+      db.close();
+    }
+  }
+
+  @Override
+  public long insert(SpeedDialEntry entry) {
+    long updateRowId;
+    try (SQLiteDatabase db = getWritableDatabase()) {
+      updateRowId = db.insert(TABLE_NAME, null, buildContentValues(entry));
+    }
+    if (updateRowId == -1) {
+      throw Assert.createUnsupportedOperationFailException(
+          "Attempted to insert a row that already exists.");
+    }
+    return updateRowId;
+  }
+
+  @Override
+  public void update(List<SpeedDialEntry> entries) {
+    SQLiteDatabase db = getWritableDatabase();
+    db.beginTransaction();
+    try {
+      for (SpeedDialEntry entry : entries) {
+        int count =
+            db.update(
+                TABLE_NAME,
+                buildContentValues(entry),
+                ID + " = ?",
+                new String[] {Long.toString(entry.id())});
+        if (count != 1) {
+          throw Assert.createUnsupportedOperationFailException(
+              "Attempted to update an undetermined number of rows: " + count);
+        }
+      }
+      db.setTransactionSuccessful();
+    } finally {
+      db.endTransaction();
+      db.close();
+    }
+  }
+
+  private ContentValues buildContentValues(SpeedDialEntry entry) {
+    ContentValues values = new ContentValues();
+    values.put(ID, entry.id());
+    values.put(CONTACT_ID, entry.contactId());
+    values.put(LOOKUP_KEY, entry.lookupKey());
+    if (entry.defaultChannel() != null) {
+      values.put(PHONE_NUMBER, entry.defaultChannel().number());
+      values.put(PHONE_LABEL, entry.defaultChannel().label());
+      values.put(PHONE_TYPE, entry.defaultChannel().technology());
+    }
+    return values;
+  }
+
+  @Override
+  public void delete(List<Long> ids) {
+    List<String> idStrings = new ArrayList<>();
+    for (Long id : ids) {
+      idStrings.add(Long.toString(id));
+    }
+
+    Selection selection = Selection.builder().and(Selection.column(ID).in(idStrings)).build();
+    try (SQLiteDatabase db = getWritableDatabase()) {
+      int count = db.delete(TABLE_NAME, selection.getSelection(), selection.getSelectionArgs());
+      if (count != ids.size()) {
+        throw Assert.createUnsupportedOperationFailException(
+            "Attempted to delete an undetermined number of rows: " + count);
+      }
+    }
+  }
+
+  @Override
+  public void deleteAll() {
+    SQLiteDatabase db = getWritableDatabase();
+    db.beginTransaction();
+    try {
+      // Passing null into where clause will delete all rows
+      db.delete(TABLE_NAME, /* whereClause=*/ null, null);
+      db.setTransactionSuccessful();
+    } finally {
+      db.endTransaction();
+      db.close();
+    }
+  }
+}
diff --git a/java/com/android/dialer/telecom/TelecomUtil.java b/java/com/android/dialer/telecom/TelecomUtil.java
index 2608cb2..f05ec20 100644
--- a/java/com/android/dialer/telecom/TelecomUtil.java
+++ b/java/com/android/dialer/telecom/TelecomUtil.java
@@ -26,6 +26,7 @@
 import android.os.Build.VERSION;
 import android.os.Build.VERSION_CODES;
 import android.provider.CallLog.Calls;
+import android.provider.Settings;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.annotation.RequiresPermission;
@@ -299,6 +300,11 @@
     return instance.isDefaultDialer(context);
   }
 
+  public static boolean isRttEnabled(Context context) {
+    return Settings.System.getInt(context.getContentResolver(), Settings.System.RTT_CALLING_MODE, 0)
+        != 0;
+  }
+
   /** @return the other SIM based PhoneAccountHandle that is not {@code currentAccount} */
   @Nullable
   @RequiresPermission(permission.READ_PHONE_STATE)
diff --git a/java/com/android/incallui/CallButtonPresenter.java b/java/com/android/incallui/CallButtonPresenter.java
index 3854642..38c8da8 100644
--- a/java/com/android/incallui/CallButtonPresenter.java
+++ b/java/com/android/incallui/CallButtonPresenter.java
@@ -304,6 +304,12 @@
   }
 
   @Override
+  public void changeToRttClicked() {
+    LogUtil.enterBlock("CallButtonPresenter.changeToRttClicked");
+    call.sendRttUpgradeRequest();
+  }
+
+  @Override
   public void onEndCallClicked() {
     LogUtil.i("CallButtonPresenter.onEndCallClicked", "call: " + call);
     if (call != null) {
@@ -473,6 +479,8 @@
             // Most devices cannot make calls on 2 SIMs at the same time.
             && InCallPresenter.getInstance().getCallList().getAllCalls().size() == 1;
 
+    boolean showUpgradeToRtt = TelecomUtil.isRttEnabled(context) && call.canUpgradeToRttCall();
+
     inCallButtonUi.showButton(InCallButtonIds.BUTTON_AUDIO, true);
     inCallButtonUi.showButton(InCallButtonIds.BUTTON_SWAP, showSwap);
     inCallButtonUi.showButton(InCallButtonIds.BUTTON_HOLD, showHold);
@@ -482,6 +490,7 @@
     inCallButtonUi.showButton(InCallButtonIds.BUTTON_ADD_CALL, true);
     inCallButtonUi.enableButton(InCallButtonIds.BUTTON_ADD_CALL, showAddCall);
     inCallButtonUi.showButton(InCallButtonIds.BUTTON_UPGRADE_TO_VIDEO, showUpgradeToVideo);
+    inCallButtonUi.showButton(InCallButtonIds.BUTTON_UPGRADE_TO_RTT, showUpgradeToRtt);
     inCallButtonUi.showButton(InCallButtonIds.BUTTON_DOWNGRADE_TO_AUDIO, showDowngradeToAudio);
     inCallButtonUi.showButton(
         InCallButtonIds.BUTTON_SWITCH_CAMERA,
diff --git a/java/com/android/incallui/CallCardPresenter.java b/java/com/android/incallui/CallCardPresenter.java
index 9c5e062..2f88c88 100644
--- a/java/com/android/incallui/CallCardPresenter.java
+++ b/java/com/android/incallui/CallCardPresenter.java
@@ -271,10 +271,10 @@
 
       // getCallToDisplay doesn't go through outgoing or incoming calls. It will return the
       // highest priority call to display as the secondary call.
-      secondary = getCallToDisplay(callList, null, true);
+      secondary = InCallPresenter.getCallToDisplay(callList, null, true);
     } else if (newState == InCallState.INCALL) {
-      primary = getCallToDisplay(callList, null, false);
-      secondary = getCallToDisplay(callList, primary, true);
+      primary = InCallPresenter.getCallToDisplay(callList, null, false);
+      secondary = InCallPresenter.getCallToDisplay(callList, primary, true);
     }
 
     LogUtil.v("CallCardPresenter.onStateChange", "primary call: " + primary);
@@ -302,7 +302,6 @@
     this.primaryNumber = primaryNumber;
 
     if (this.primary != null) {
-      InCallPresenter.getInstance().onForegroundCallChanged(this.primary);
       inCallScreen.updateInCallScreenColors();
     }
 
@@ -636,54 +635,6 @@
     }
   }
 
-  /**
-   * Get the highest priority call to display. Goes through the calls and chooses which to return
-   * based on priority of which type of call to display to the user. Callers can use the "ignore"
-   * feature to get the second best call by passing a previously found primary call as ignore.
-   *
-   * @param ignore A call to ignore if found.
-   */
-  private DialerCall getCallToDisplay(
-      CallList callList, DialerCall ignore, boolean skipDisconnected) {
-    // Active calls come second.  An active call always gets precedent.
-    DialerCall retval = callList.getActiveCall();
-    if (retval != null && retval != ignore) {
-      return retval;
-    }
-
-    // Sometimes there is intemediate state that two calls are in active even one is about
-    // to be on hold.
-    retval = callList.getSecondActiveCall();
-    if (retval != null && retval != ignore) {
-      return retval;
-    }
-
-    // Disconnected calls get primary position if there are no active calls
-    // to let user know quickly what call has disconnected. Disconnected
-    // calls are very short lived.
-    if (!skipDisconnected) {
-      retval = callList.getDisconnectingCall();
-      if (retval != null && retval != ignore) {
-        return retval;
-      }
-      retval = callList.getDisconnectedCall();
-      if (retval != null && retval != ignore) {
-        return retval;
-      }
-    }
-
-    // Then we go to background call (calls on hold)
-    retval = callList.getBackgroundCall();
-    if (retval != null && retval != ignore) {
-      return retval;
-    }
-
-    // Lastly, we go to a second background call.
-    retval = callList.getSecondBackgroundCall();
-
-    return retval;
-  }
-
   private void updatePrimaryDisplayInfo() {
     if (inCallScreen == null) {
       // TODO: May also occur if search result comes back after ui is destroyed. Look into
diff --git a/java/com/android/incallui/InCallActivity.java b/java/com/android/incallui/InCallActivity.java
index 9d08dc4..1ba3683 100644
--- a/java/com/android/incallui/InCallActivity.java
+++ b/java/com/android/incallui/InCallActivity.java
@@ -1468,7 +1468,7 @@
       return new ShouldShowUiResult(false, null);
     }
 
-    if (call.isRttCall()) {
+    if (call.isActiveRttCall()) {
       LogUtil.i("InCallActivity.getShouldShowRttUi", "found rtt call");
       return new ShouldShowUiResult(true, call);
     }
@@ -1520,7 +1520,7 @@
     AnswerScreen answerScreen =
         AnswerBindings.createAnswerScreen(
             call.getId(),
-            call.isRttCall(),
+            call.isActiveRttCall(),
             call.isVideoCall(),
             isVideoUpgradeRequest,
             call.getVideoTech().isSelfManagedCamera(),
diff --git a/java/com/android/incallui/InCallPresenter.java b/java/com/android/incallui/InCallPresenter.java
index 2e98a96..e11b376 100644
--- a/java/com/android/incallui/InCallPresenter.java
+++ b/java/com/android/incallui/InCallPresenter.java
@@ -771,6 +771,22 @@
         "Phone switching state: " + oldState + " -> " + newState);
     inCallState = newState;
 
+    // Foreground call changed
+    DialerCall primary = null;
+    if (newState == InCallState.INCOMING) {
+      primary = callList.getIncomingCall();
+    } else if (newState == InCallState.PENDING_OUTGOING || newState == InCallState.OUTGOING) {
+      primary = callList.getOutgoingCall();
+      if (primary == null) {
+        primary = callList.getPendingOutgoingCall();
+      }
+    } else if (newState == InCallState.INCALL) {
+      primary = getCallToDisplay(callList, null, false);
+    }
+    if (primary != null) {
+      onForegroundCallChanged(primary);
+    }
+
     // notify listeners of new state
     for (InCallStateListener listener : listeners) {
       LogUtil.d(
@@ -787,6 +803,54 @@
     Trace.endSection();
   }
 
+  /**
+   * Get the highest priority call to display. Goes through the calls and chooses which to return
+   * based on priority of which type of call to display to the user. Callers can use the "ignore"
+   * feature to get the second best call by passing a previously found primary call as ignore.
+   *
+   * @param ignore A call to ignore if found.
+   */
+  static DialerCall getCallToDisplay(
+      CallList callList, DialerCall ignore, boolean skipDisconnected) {
+    // Active calls come second.  An active call always gets precedent.
+    DialerCall retval = callList.getActiveCall();
+    if (retval != null && retval != ignore) {
+      return retval;
+    }
+
+    // Sometimes there is intemediate state that two calls are in active even one is about
+    // to be on hold.
+    retval = callList.getSecondActiveCall();
+    if (retval != null && retval != ignore) {
+      return retval;
+    }
+
+    // Disconnected calls get primary position if there are no active calls
+    // to let user know quickly what call has disconnected. Disconnected
+    // calls are very short lived.
+    if (!skipDisconnected) {
+      retval = callList.getDisconnectingCall();
+      if (retval != null && retval != ignore) {
+        return retval;
+      }
+      retval = callList.getDisconnectedCall();
+      if (retval != null && retval != ignore) {
+        return retval;
+      }
+    }
+
+    // Then we go to background call (calls on hold)
+    retval = callList.getBackgroundCall();
+    if (retval != null && retval != ignore) {
+      return retval;
+    }
+
+    // Lastly, we go to a second background call.
+    retval = callList.getSecondBackgroundCall();
+
+    return retval;
+  }
+
   /** Called when there is a new incoming call. */
   @Override
   public void onIncomingCall(DialerCall call) {
diff --git a/java/com/android/incallui/ProximitySensor.java b/java/com/android/incallui/ProximitySensor.java
index 4b03344..9719e5d 100644
--- a/java/com/android/incallui/ProximitySensor.java
+++ b/java/com/android/incallui/ProximitySensor.java
@@ -113,7 +113,7 @@
 
     DialerCall activeCall = callList.getActiveCall();
     boolean isVideoCall = activeCall != null && activeCall.isVideoCall();
-    boolean isRttCall = activeCall != null && activeCall.isRttCall();
+    boolean isRttCall = activeCall != null && activeCall.isActiveRttCall();
 
     if (isOffhook != isPhoneOffhook
         || this.isVideoCall != isVideoCall
diff --git a/java/com/android/incallui/ReturnToCallController.java b/java/com/android/incallui/ReturnToCallController.java
index d5e6a10..0850e91 100644
--- a/java/com/android/incallui/ReturnToCallController.java
+++ b/java/com/android/incallui/ReturnToCallController.java
@@ -92,10 +92,10 @@
     endCall = createActionIntent(ReturnToCallActionReceiver.ACTION_END_CALL);
     fullScreen = createActionIntent(ReturnToCallActionReceiver.ACTION_RETURN_TO_CALL);
 
-    InCallPresenter.getInstance().addInCallUiListener(this);
-    CallList.getInstance().addListener(this);
     AudioModeProvider.getInstance().addListener(this);
     audioState = AudioModeProvider.getInstance().getAudioState();
+    InCallPresenter.getInstance().addInCallUiListener(this);
+    CallList.getInstance().addListener(this);
   }
 
   public void tearDown() {
@@ -186,7 +186,7 @@
       return;
     }
 
-    if ((bubble == null || !bubble.isVisible())
+    if ((bubble == null || !(bubble.isVisible() || bubble.isDismissed()))
         && 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 f639e5b..01f3b9d 100644
--- a/java/com/android/incallui/call/CallList.java
+++ b/java/com/android/incallui/call/CallList.java
@@ -514,6 +514,15 @@
     return call != null && call != getDisconnectingCall() && call != getDisconnectedCall();
   }
 
+  boolean hasActiveRttCall() {
+    for (DialerCall call : getAllCalls()) {
+      if (call.isActiveRttCall()) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   /**
    * Returns the first call found in the call map with the upgrade to video modification state.
    *
diff --git a/java/com/android/incallui/call/DialerCall.java b/java/com/android/incallui/call/DialerCall.java
index 4815a6e..e08c926 100644
--- a/java/com/android/incallui/call/DialerCall.java
+++ b/java/com/android/incallui/call/DialerCall.java
@@ -988,7 +988,7 @@
   }
 
   @TargetApi(28)
-  public boolean isRttCall() {
+  public boolean isActiveRttCall() {
     if (BuildCompat.isAtLeastP()) {
       return getTelecomCall().isRttActive();
     } else {
@@ -998,12 +998,41 @@
 
   @TargetApi(28)
   public RttCall getRttCall() {
-    if (!isRttCall()) {
+    if (!isActiveRttCall()) {
       return null;
     }
     return getTelecomCall().getRttCall();
   }
 
+  @TargetApi(28)
+  public boolean canUpgradeToRttCall() {
+    PhoneAccount phoneAccount = getPhoneAccount();
+    if (phoneAccount == null) {
+      return false;
+    }
+    if (!phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_RTT)) {
+      return false;
+    }
+    if (isActiveRttCall()) {
+      return false;
+    }
+    if (isVideoCall()) {
+      return false;
+    }
+    if (isConferenceCall()) {
+      return false;
+    }
+    if (CallList.getInstance().hasActiveRttCall()) {
+      return false;
+    }
+    return true;
+  }
+
+  @TargetApi(28)
+  public void sendRttUpgradeRequest() {
+    getTelecomCall().sendRttRequest();
+  }
+
   public boolean hasReceivedVideoUpgradeRequest() {
     return VideoUtils.hasReceivedVideoUpgradeRequest(getVideoTech().getSessionModificationState());
   }
diff --git a/java/com/android/incallui/callpending/CallPendingActivity.java b/java/com/android/incallui/callpending/CallPendingActivity.java
index c7ce2b1..831ebbd 100644
--- a/java/com/android/incallui/callpending/CallPendingActivity.java
+++ b/java/com/android/incallui/callpending/CallPendingActivity.java
@@ -255,6 +255,9 @@
           public void changeToVideoClicked() {}
 
           @Override
+          public void changeToRttClicked() {}
+
+          @Override
           public void switchCameraClicked(boolean useFrontFacingCamera) {}
 
           @Override
diff --git a/java/com/android/incallui/incall/impl/ButtonChooserFactory.java b/java/com/android/incallui/incall/impl/ButtonChooserFactory.java
index 2a08940..757d813 100644
--- a/java/com/android/incallui/incall/impl/ButtonChooserFactory.java
+++ b/java/com/android/incallui/incall/impl/ButtonChooserFactory.java
@@ -57,6 +57,9 @@
     mapping.put(
         InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE,
         MappingInfo.builder(4).setSlotOrder(0).build());
+    // RTT call is only supported on IMS and WiFi.
+    mapping.put(
+        InCallButtonIds.BUTTON_UPGRADE_TO_RTT, MappingInfo.builder(3).setSlotOrder(0).build());
     mapping.put(
         InCallButtonIds.BUTTON_UPGRADE_TO_VIDEO, MappingInfo.builder(4).setSlotOrder(10).build());
     mapping.put(
@@ -114,7 +117,7 @@
     mapping.put(InCallButtonIds.BUTTON_MUTE, MappingInfo.builder(0).build());
     mapping.put(InCallButtonIds.BUTTON_DIALPAD, MappingInfo.builder(1).build());
     mapping.put(InCallButtonIds.BUTTON_AUDIO, MappingInfo.builder(2).build());
-    mapping.put(InCallButtonIds.BUTTON_MERGE, MappingInfo.builder(3).setSlotOrder(0).build());
+    mapping.put(InCallButtonIds.BUTTON_MERGE, MappingInfo.builder(3).setSlotOrder(5).build());
     mapping.put(InCallButtonIds.BUTTON_ADD_CALL, MappingInfo.builder(3).build());
     mapping.put(InCallButtonIds.BUTTON_SWAP_SIM, MappingInfo.builder(4).build());
     return mapping;
diff --git a/java/com/android/incallui/incall/impl/ButtonController.java b/java/com/android/incallui/incall/impl/ButtonController.java
index 98460c7..9106dab 100644
--- a/java/com/android/incallui/incall/impl/ButtonController.java
+++ b/java/com/android/incallui/incall/impl/ButtonController.java
@@ -519,6 +519,24 @@
     }
   }
 
+  class UpgradeToRttButtonController extends SimpleNonCheckableButtonController {
+
+    public UpgradeToRttButtonController(@NonNull InCallButtonUiDelegate delegate) {
+      super(
+          delegate,
+          InCallButtonIds.BUTTON_UPGRADE_TO_RTT,
+          0,
+          R.string.incall_label_rttcall,
+          R.drawable.quantum_ic_rtt_vd_theme_24);
+      Assert.isNotNull(delegate);
+    }
+
+    @Override
+    public void onClick(View view) {
+      delegate.changeToRttClicked();
+    }
+  }
+
   class ManageConferenceButtonController extends SimpleNonCheckableButtonController {
 
     private final InCallScreenDelegate inCallScreenDelegate;
diff --git a/java/com/android/incallui/incall/impl/InCallFragment.java b/java/com/android/incallui/incall/impl/InCallFragment.java
index fb8c2c4..6f0ba60 100644
--- a/java/com/android/incallui/incall/impl/InCallFragment.java
+++ b/java/com/android/incallui/incall/impl/InCallFragment.java
@@ -54,6 +54,7 @@
 import com.android.incallui.contactgrid.ContactGridManager;
 import com.android.incallui.hold.OnHoldFragment;
 import com.android.incallui.incall.impl.ButtonController.SpeakerButtonController;
+import com.android.incallui.incall.impl.ButtonController.UpgradeToRttButtonController;
 import com.android.incallui.incall.impl.InCallButtonGridFragment.OnButtonGridCreatedListener;
 import com.android.incallui.incall.protocol.InCallButtonIds;
 import com.android.incallui.incall.protocol.InCallButtonIdsExtension;
@@ -114,7 +115,8 @@
         || id == InCallButtonIds.BUTTON_ADD_CALL
         || id == InCallButtonIds.BUTTON_MERGE
         || id == InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE
-        || id == InCallButtonIds.BUTTON_SWAP_SIM;
+        || id == InCallButtonIds.BUTTON_SWAP_SIM
+        || id == InCallButtonIds.BUTTON_UPGRADE_TO_RTT;
   }
 
   @Override
@@ -226,6 +228,7 @@
     buttonControllers.add(new ButtonController.SwapSimButtonController(inCallButtonUiDelegate));
     buttonControllers.add(
         new ButtonController.UpgradeToVideoButtonController(inCallButtonUiDelegate));
+    buttonControllers.add(new UpgradeToRttButtonController(inCallButtonUiDelegate));
     buttonControllers.add(
         new ButtonController.ManageConferenceButtonController(inCallScreenDelegate));
     buttonControllers.add(
diff --git a/java/com/android/incallui/incall/impl/res/values/strings.xml b/java/com/android/incallui/incall/impl/res/values/strings.xml
index d021756..c4c40a1 100644
--- a/java/com/android/incallui/incall/impl/res/values/strings.xml
+++ b/java/com/android/incallui/incall/impl/res/values/strings.xml
@@ -20,6 +20,10 @@
      [CHAR LIMIT=12] -->
   <string name="incall_label_videocall">Video call</string>
 
+  <!-- Button shown during a phone call to upgrade to Real-time Text.
+     [CHAR LIMIT=12] -->
+  <string name="incall_label_rttcall">RTT</string>
+
   <!-- Button shown during a phone call to put the call on hold.
      [CHAR LIMIT=12] -->
   <string name="incall_label_hold">Hold</string>
diff --git a/java/com/android/incallui/incall/protocol/InCallButtonIds.java b/java/com/android/incallui/incall/protocol/InCallButtonIds.java
index 3de5335..80ea75e 100644
--- a/java/com/android/incallui/incall/protocol/InCallButtonIds.java
+++ b/java/com/android/incallui/incall/protocol/InCallButtonIds.java
@@ -39,6 +39,7 @@
   InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY,
   InCallButtonIds.BUTTON_SWAP_SIM,
   InCallButtonIds.BUTTON_COUNT,
+  InCallButtonIds.BUTTON_UPGRADE_TO_RTT
 })
 public @interface InCallButtonIds {
 
@@ -58,4 +59,5 @@
   int BUTTON_SWITCH_TO_SECONDARY = 13;
   int BUTTON_SWAP_SIM = 14;
   int BUTTON_COUNT = 15;
+  int BUTTON_UPGRADE_TO_RTT = 16;
 }
diff --git a/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java b/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java
index db6e900..5239d9d 100644
--- a/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java
+++ b/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java
@@ -56,6 +56,8 @@
       return "SWITCH_TO_SECONDARY";
     } else if (id == InCallButtonIds.BUTTON_SWAP_SIM) {
       return "SWAP_SIM";
+    } else if (id == InCallButtonIds.BUTTON_UPGRADE_TO_RTT) {
+      return "UPGRADE_TO_RTT";
     } else {
       return "INVALID_BUTTON: " + id;
     }
diff --git a/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java b/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java
index 9f9c5fb..b0e23ef 100644
--- a/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java
+++ b/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java
@@ -47,6 +47,8 @@
 
   void changeToVideoClicked();
 
+  void changeToRttClicked();
+
   void switchCameraClicked(boolean useFrontFacingCamera);
 
   void toggleCameraClicked();
diff --git a/java/com/android/incallui/rtt/impl/AudioSelectMenu.java b/java/com/android/incallui/rtt/impl/AudioSelectMenu.java
index 2d4ab39..01c3950 100644
--- a/java/com/android/incallui/rtt/impl/AudioSelectMenu.java
+++ b/java/com/android/incallui/rtt/impl/AudioSelectMenu.java
@@ -39,7 +39,7 @@
       Context context,
       InCallButtonUiDelegate inCallButtonUiDelegate,
       OnButtonClickListener onButtonClickListener) {
-    super(context);
+    super(context, null, 0, R.style.OverflowMenu);
     this.context = context;
     this.inCallButtonUiDelegate = inCallButtonUiDelegate;
     this.onButtonClickListener = onButtonClickListener;
@@ -76,7 +76,6 @@
     }
     item.setOnClickListener(
         (v) -> {
-          dismiss();
           inCallButtonUiDelegate.setAudioRoute(itemRoute);
         });
   }
diff --git a/java/com/android/incallui/rtt/impl/RttChatAdapter.java b/java/com/android/incallui/rtt/impl/RttChatAdapter.java
index 8d924c9..955fc9f 100644
--- a/java/com/android/incallui/rtt/impl/RttChatAdapter.java
+++ b/java/com/android/incallui/rtt/impl/RttChatAdapter.java
@@ -33,7 +33,9 @@
 public class RttChatAdapter extends RecyclerView.Adapter<RttChatMessageViewHolder> {
 
   interface MessageListener {
-    void newMessageAdded();
+    void onUpdateRemoteMessage(int position);
+
+    void onUpdateLocalMessage(int position);
   }
 
   private static final String KEY_MESSAGE_DATA = "key_message_data";
@@ -114,7 +116,7 @@
   void addLocalMessage(String message) {
     updateCurrentLocalMessage(message);
     if (messageListener != null) {
-      messageListener.newMessageAdded();
+      messageListener.onUpdateLocalMessage(lastIndexOfLocalMessage);
     }
   }
 
@@ -143,7 +145,7 @@
     }
     updateCurrentRemoteMessage(message);
     if (messageListener != null) {
-      messageListener.newMessageAdded();
+      messageListener.onUpdateRemoteMessage(RttChatMessage.getLastIndexRemoteMessage(rttMessages));
     }
   }
 
diff --git a/java/com/android/incallui/rtt/impl/RttChatFragment.java b/java/com/android/incallui/rtt/impl/RttChatFragment.java
index 56ac242..a181f88 100644
--- a/java/com/android/incallui/rtt/impl/RttChatFragment.java
+++ b/java/com/android/incallui/rtt/impl/RttChatFragment.java
@@ -85,15 +85,6 @@
   private ImageButton submitButton;
   private boolean isClearingInput;
 
-  private final OnScrollListener onScrollListener =
-      new OnScrollListener() {
-        @Override
-        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
-          if (dy < 0) {
-            UiUtil.hideKeyboardFrom(getContext(), editText);
-          }
-        }
-      };
   private InCallScreenDelegate inCallScreenDelegate;
   private RttCallScreenDelegate rttCallScreenDelegate;
   private InCallButtonUiDelegate inCallButtonUiDelegate;
@@ -105,6 +96,8 @@
   private SecondaryInfo savedSecondaryInfo;
   private TextView statusBanner;
   private PrimaryInfo primaryInfo;
+  private boolean isUserScrolling;
+  private boolean shouldAutoScrolling;
 
   /**
    * Create a new instance of RttChatFragment.
@@ -193,7 +186,27 @@
     recyclerView.setHasFixedSize(false);
     adapter = new RttChatAdapter(getContext(), this, savedInstanceState);
     recyclerView.setAdapter(adapter);
-    recyclerView.addOnScrollListener(onScrollListener);
+    recyclerView.addOnScrollListener(
+        new OnScrollListener() {
+          @Override
+          public void onScrollStateChanged(RecyclerView recyclerView, int i) {
+            if (i == RecyclerView.SCROLL_STATE_DRAGGING) {
+              isUserScrolling = true;
+            } else if (i == RecyclerView.SCROLL_STATE_IDLE) {
+              isUserScrolling = false;
+              // Auto scrolling for new messages should be resumed if it's scrolled to bottom.
+              shouldAutoScrolling = !recyclerView.canScrollVertically(1);
+            }
+          }
+
+          @Override
+          public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+            if (dy < 0 && isUserScrolling) {
+              UiUtil.hideKeyboardFrom(getContext(), editText);
+            }
+          }
+        });
+
     submitButton = view.findViewById(R.id.rtt_chat_submit_button);
     submitButton.setOnClickListener(
         v -> {
@@ -202,6 +215,9 @@
           editText.setText("");
           isClearingInput = false;
           rttCallScreenDelegate.onLocalMessage(Constants.BUBBLE_BREAKER);
+          // Auto scrolling for new messages should be resumed since user has submit current
+          // message.
+          shouldAutoScrolling = true;
         });
     submitButton.setEnabled(false);
     endCallButton = view.findViewById(R.id.rtt_end_call_button);
@@ -276,8 +292,21 @@
   }
 
   @Override
-  public void newMessageAdded() {
-    recyclerView.smoothScrollToPosition(adapter.getItemCount());
+  public void onUpdateLocalMessage(int position) {
+    if (position < 0) {
+      return;
+    }
+    recyclerView.smoothScrollToPosition(position);
+  }
+
+  @Override
+  public void onUpdateRemoteMessage(int position) {
+    if (position < 0) {
+      return;
+    }
+    if (shouldAutoScrolling) {
+      recyclerView.smoothScrollToPosition(position);
+    }
   }
 
   @Override
diff --git a/java/com/android/incallui/rtt/impl/RttChatMessage.java b/java/com/android/incallui/rtt/impl/RttChatMessage.java
index cbc53ef..0060b1b 100644
--- a/java/com/android/incallui/rtt/impl/RttChatMessage.java
+++ b/java/com/android/incallui/rtt/impl/RttChatMessage.java
@@ -162,7 +162,7 @@
     return i;
   }
 
-  private static int getLastIndexRemoteMessage(List<RttChatMessage> messageList) {
+  static int getLastIndexRemoteMessage(List<RttChatMessage> messageList) {
     int i = messageList.size() - 1;
     while (i >= 0 && !messageList.get(i).isRemote) {
       i--;
diff --git a/java/com/android/incallui/rtt/impl/RttOverflowMenu.java b/java/com/android/incallui/rtt/impl/RttOverflowMenu.java
index 6a7aeba..deee8ee 100644
--- a/java/com/android/incallui/rtt/impl/RttOverflowMenu.java
+++ b/java/com/android/incallui/rtt/impl/RttOverflowMenu.java
@@ -42,7 +42,7 @@
       Context context,
       InCallButtonUiDelegate inCallButtonUiDelegate,
       InCallScreenDelegate inCallScreenDelegate) {
-    super(context);
+    super(context, null, 0, R.style.OverflowMenu);
     this.inCallButtonUiDelegate = inCallButtonUiDelegate;
     this.inCallScreenDelegate = inCallScreenDelegate;
     View view = View.inflate(context, R.layout.overflow_menu, null);
@@ -67,7 +67,6 @@
           if (isSwitchToSecondaryButtonEnabled) {
             this.inCallScreenDelegate.onSecondaryInfoClicked();
           }
-          dismiss();
         });
   }
 
@@ -80,7 +79,6 @@
     } else if (button == dialpadButton) {
       inCallButtonUiDelegate.showDialpadClicked(isChecked);
     }
-    dismiss();
   }
 
   void setMuteButtonChecked(boolean isChecked) {
diff --git a/java/com/android/incallui/rtt/impl/res/drawable/overflow_menu_background.xml b/java/com/android/incallui/rtt/impl/res/drawable/overflow_menu_background.xml
index 6142986..2af14fd 100644
--- a/java/com/android/incallui/rtt/impl/res/drawable/overflow_menu_background.xml
+++ b/java/com/android/incallui/rtt/impl/res/drawable/overflow_menu_background.xml
@@ -18,4 +18,4 @@
     android:shape="rectangle">
   <solid android:color="@android:color/white"/>
   <corners android:radius="2dp"/>
-</shape>
\ No newline at end of file
+</shape>
diff --git a/java/com/android/incallui/rtt/impl/res/layout/audio_route.xml b/java/com/android/incallui/rtt/impl/res/layout/audio_route.xml
index 89b5c76..f098316 100644
--- a/java/com/android/incallui/rtt/impl/res/layout/audio_route.xml
+++ b/java/com/android/incallui/rtt/impl/res/layout/audio_route.xml
@@ -20,7 +20,6 @@
     android:layout_height="wrap_content"
     android:paddingTop="8dp"
     android:paddingBottom="8dp"
-    android:background="@drawable/overflow_menu_background"
     android:orientation="vertical">
   <com.android.incallui.rtt.impl.RttCheckableButton
       android:id="@+id/audioroute_back"
diff --git a/java/com/android/incallui/rtt/impl/res/layout/frag_rtt_chat.xml b/java/com/android/incallui/rtt/impl/res/layout/frag_rtt_chat.xml
index 34a9954..ea7ff10 100644
--- a/java/com/android/incallui/rtt/impl/res/layout/frag_rtt_chat.xml
+++ b/java/com/android/incallui/rtt/impl/res/layout/frag_rtt_chat.xml
@@ -23,6 +23,7 @@
       android:id="@+id/rtt_recycler_view"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
+      android:paddingTop="70dp"
       android:paddingBottom="70dp"
       android:clipToPadding="false"/>
 
diff --git a/java/com/android/incallui/rtt/impl/res/layout/overflow_menu.xml b/java/com/android/incallui/rtt/impl/res/layout/overflow_menu.xml
index eb7e386..0ec36f3 100644
--- a/java/com/android/incallui/rtt/impl/res/layout/overflow_menu.xml
+++ b/java/com/android/incallui/rtt/impl/res/layout/overflow_menu.xml
@@ -20,7 +20,6 @@
     android:layout_height="wrap_content"
     android:paddingTop="8dp"
     android:paddingBottom="8dp"
-    android:background="@drawable/overflow_menu_background"
     android:orientation="vertical">
   <com.android.incallui.rtt.impl.RttCheckableButton
       android:id="@+id/menu_mute"
diff --git a/java/com/android/incallui/rtt/impl/res/values/dimens.xml b/java/com/android/incallui/rtt/impl/res/values/dimens.xml
index 4c3fe02..a6418d7 100644
--- a/java/com/android/incallui/rtt/impl/res/values/dimens.xml
+++ b/java/com/android/incallui/rtt/impl/res/values/dimens.xml
@@ -18,4 +18,5 @@
   <dimen name="rtt_message_margin_top">16dp</dimen>
   <dimen name="rtt_same_group_message_margin_top">2dp</dimen>
   <dimen name="rtt_overflow_menu_width">180dp</dimen>
+  <dimen name="rtt_overflow_menu_elevation">8dp</dimen>
 </resources>
\ No newline at end of file
diff --git a/java/com/android/incallui/rtt/impl/res/values/styles.xml b/java/com/android/incallui/rtt/impl/res/values/styles.xml
index bbacde8..667cd12 100644
--- a/java/com/android/incallui/rtt/impl/res/values/styles.xml
+++ b/java/com/android/incallui/rtt/impl/res/values/styles.xml
@@ -40,4 +40,10 @@
     <item name="android:theme">@style/ButtonTheme</item>
     <item name="android:background">?attr/selectableItemBackground</item>
   </style>
+
+  <style name="OverflowMenu">
+    <item name="android:popupAnimationStyle">@android:style/Animation.Dialog</item>
+    <item name="android:popupBackground">@drawable/overflow_menu_background</item>
+    <item name="android:popupElevation">@dimen/rtt_overflow_menu_elevation</item>
+  </style>
 </resources>
\ No newline at end of file
diff --git a/java/com/android/incallui/videotech/ims/ImsVideoTech.java b/java/com/android/incallui/videotech/ims/ImsVideoTech.java
index d9660e1..c4c177f 100644
--- a/java/com/android/incallui/videotech/ims/ImsVideoTech.java
+++ b/java/com/android/incallui/videotech/ims/ImsVideoTech.java
@@ -203,6 +203,7 @@
     LogUtil.enterBlock("ImsVideoTech.declineUpgradeRequest");
     call.getVideoCall()
         .sendSessionModifyResponse(new VideoProfile(call.getDetails().getVideoState()));
+    setSessionModificationState(SessionModificationState.NO_REQUEST);
     logger.logImpression(DialerImpression.Type.IMS_VIDEO_REQUEST_DECLINED);
   }
 
diff --git a/java/com/android/voicemail/impl/transcribe/GetTranscriptReceiver.java b/java/com/android/voicemail/impl/transcribe/GetTranscriptReceiver.java
index cbf1657..5c5bae5 100644
--- a/java/com/android/voicemail/impl/transcribe/GetTranscriptReceiver.java
+++ b/java/com/android/voicemail/impl/transcribe/GetTranscriptReceiver.java
@@ -295,7 +295,7 @@
     transcriptionClientFactoryForTesting = factory;
   }
 
-  private static TranscriptionClientFactory getTranscriptionClientFactory(Context context) {
+  static TranscriptionClientFactory getTranscriptionClientFactory(Context context) {
     if (transcriptionClientFactoryForTesting != null) {
       return transcriptionClientFactoryForTesting;
     }
diff --git a/java/com/android/voicemail/impl/transcribe/TranscriptionTaskAsync.java b/java/com/android/voicemail/impl/transcribe/TranscriptionTaskAsync.java
index f6035fd..034af6b 100644
--- a/java/com/android/voicemail/impl/transcribe/TranscriptionTaskAsync.java
+++ b/java/com/android/voicemail/impl/transcribe/TranscriptionTaskAsync.java
@@ -83,6 +83,16 @@
     } else if (uploadResponse == null) {
       VvmLog.i(TAG, "getTranscription, failed to upload voicemail.");
       return new Pair<>(null, TranscriptionStatus.FAILED_NO_RETRY);
+    } else if (uploadResponse.isStatusAlreadyExists()) {
+      VvmLog.i(TAG, "getTranscription, transcription already exists.");
+      GetTranscriptReceiver.beginPolling(
+          context,
+          voicemailUri,
+          uploadRequest.getTranscriptionId(),
+          0,
+          configProvider,
+          phoneAccountHandle);
+      return new Pair<>(null, null);
     } else if (uploadResponse.getTranscriptionId() == null) {
       VvmLog.i(TAG, "getTranscription, upload error: " + uploadResponse.status);
       return new Pair<>(null, TranscriptionStatus.FAILED_NO_RETRY);
@@ -116,7 +126,9 @@
     // Generate the transcript id locally if configured to do so, or if voicemail donation is
     // available (because rating donating voicemails requires locally generated voicemail ids).
     if (configProvider.useClientGeneratedVoicemailIds()
-        || configProvider.isVoicemailDonationAvailable()) {
+        || VoicemailComponent.get(context)
+            .getVoicemailClient()
+            .isVoicemailDonationAvailable(context, phoneAccountHandle)) {
       // The server currently can't handle repeated transcription id's so if we add the Uri to the
       // fingerprint (which contains the voicemail id) which is different each time a voicemail is
       // downloaded.  If this becomes a problem then it should be possible to change the server
diff --git a/java/com/android/voicemail/impl/transcribe/grpc/TranscriptionResponse.java b/java/com/android/voicemail/impl/transcribe/grpc/TranscriptionResponse.java
index ae4796d..bd65abe 100644
--- a/java/com/android/voicemail/impl/transcribe/grpc/TranscriptionResponse.java
+++ b/java/com/android/voicemail/impl/transcribe/grpc/TranscriptionResponse.java
@@ -18,6 +18,7 @@
 import android.support.annotation.Nullable;
 import com.android.dialer.common.Assert;
 import io.grpc.Status;
+import io.grpc.Status.Code;
 
 /**
  * Base class for encapulating a voicemail transcription server response. This handles the Grpc
@@ -43,6 +44,14 @@
     return false;
   }
 
+  public boolean isStatusAlreadyExists() {
+    if (status != null) {
+      return status.getCode() == Code.ALREADY_EXISTS;
+    }
+
+    return false;
+  }
+
   public boolean hasFatalError() {
     if (status != null) {
       return status.getCode() != Status.Code.OK && status.getCode() != Status.Code.UNAVAILABLE;