Merge "Don't use guava prebuilts outside of annotations processing."
diff --git a/Android.mk b/Android.mk
index 461f122..0f13891 100644
--- a/Android.mk
+++ b/Android.mk
@@ -15,14 +15,6 @@
 # The base directory for Dialer sources.
 BASE_DIR := java/com/android
 
-# Primary dialer module sources.
-SRC_DIRS := \
-	$(BASE_DIR)/contacts/common \
-	$(BASE_DIR)/dialer \
-	$(BASE_DIR)/dialershared \
-	$(BASE_DIR)/incallui \
-	$(BASE_DIR)/voicemail
-
 # Exclude files incompatible with AOSP.
 EXCLUDE_FILES := \
 	$(BASE_DIR)/incallui/calllocation/impl/AuthException.java \
@@ -76,8 +68,8 @@
 LOCAL_FULL_LIBS_MANIFEST_FILES := \
 	$(addprefix $(LOCAL_PATH)/, $(DIALER_MANIFEST_FILES))
 
-LOCAL_SRC_FILES := $(call all-java-files-under, $(SRC_DIRS))
-LOCAL_SRC_FILES += $(call all-proto-files-under, $(SRC_DIRS))
+LOCAL_SRC_FILES := $(call all-java-files-under, $(BASE_DIR))
+LOCAL_SRC_FILES += $(call all-proto-files-under, $(BASE_DIR))
 LOCAL_SRC_FILES := $(filter-out $(EXCLUDE_FILES),$(LOCAL_SRC_FILES))
 
 LOCAL_PROTOC_FLAGS := --proto_path=$(LOCAL_PATH)
@@ -93,6 +85,7 @@
 # We specify each package explicitly to glob resource files.
 # find . -type f -name "AndroidManifest.xml" | uniq | sort | cut -c 8- | rev | cut -c 21- | rev | sed 's/\//./g' | sed 's/$/ \\/'
 LOCAL_AAPT_FLAGS := \
+	com.android.bubble \
 	com.android.contacts.common \
 	com.android.dialer.about \
 	com.android.dialer.app \
@@ -131,7 +124,6 @@
 	com.android.dialer.searchfragment.list \
 	com.android.dialer.searchfragment.nearbyplaces \
 	com.android.dialer.searchfragment.remote \
-	com.android.dialershared.bubble \
 	com.android.dialer.shortcuts \
 	com.android.dialer.simulator.impl \
 	com.android.dialer.speeddial \
@@ -233,7 +225,7 @@
 
 
 # Proguard includes
-LOCAL_PROGUARD_FLAG_FILES := $(call all-named-files-under,proguard.*flags,$(SRC_DIRS))
+LOCAL_PROGUARD_FLAG_FILES := $(call all-named-files-under,proguard.*flags,$(BASE_DIR))
 LOCAL_PROGUARD_ENABLED := custom
 
 LOCAL_PROGUARD_ENABLED += optimization
@@ -255,7 +247,6 @@
 
 # Cleanup local state
 BASE_DIR :=
-SRC_DIRS :=
 EXCLUDE_FILES :=
 RES_DIRS :=
 DIALER_MANIFEST_FILES :=
diff --git a/java/com/android/dialershared/bubble/AndroidManifest.xml b/java/com/android/bubble/AndroidManifest.xml
similarity index 94%
rename from java/com/android/dialershared/bubble/AndroidManifest.xml
rename to java/com/android/bubble/AndroidManifest.xml
index 1a94aaf..80efe5c 100644
--- a/java/com/android/dialershared/bubble/AndroidManifest.xml
+++ b/java/com/android/bubble/AndroidManifest.xml
@@ -15,7 +15,7 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.dialershared.bubble">
+    package="com.android.bubble">
 
   <uses-sdk android:minSdkVersion="21"/>
   <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
diff --git a/java/com/android/dialershared/bubble/Bubble.java b/java/com/android/bubble/Bubble.java
similarity index 95%
rename from java/com/android/dialershared/bubble/Bubble.java
rename to java/com/android/bubble/Bubble.java
index d245522..9abfa43 100644
--- a/java/com/android/dialershared/bubble/Bubble.java
+++ b/java/com/android/bubble/Bubble.java
@@ -14,7 +14,7 @@
  * limitations under the License
  */
 
-package com.android.dialershared.bubble;
+package com.android.bubble;
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
@@ -61,7 +61,7 @@
 import android.widget.ImageView;
 import android.widget.TextView;
 import android.widget.ViewAnimator;
-import com.android.dialershared.bubble.BubbleInfo.Action;
+import com.android.bubble.BubbleInfo.Action;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.List;
@@ -87,8 +87,7 @@
   private final Context context;
   private final WindowManager windowManager;
 
-  private final Handler handler = new Handler();
-
+  private final Handler handler;
   private LayoutParams windowParams;
 
   // Initialized in factory method
@@ -100,6 +99,7 @@
   private boolean expanded;
   private boolean textShowing;
   private boolean hideAfterText;
+  private CharSequence textAfterShow;
   private int collapseEndAction;
 
   @VisibleForTesting ViewHolder viewHolder;
@@ -107,6 +107,21 @@
   private Integer overrideGravity;
   private ViewPropertyAnimator exitAnimator;
 
+  private final Runnable collapseRunnable =
+      new Runnable() {
+        @Override
+        public void run() {
+          textShowing = false;
+          if (hideAfterText) {
+            // Always reset here since text shouldn't keep showing.
+            hideAndReset();
+          } else {
+            doResize(
+                () -> viewHolder.getPrimaryButton().setDisplayedChild(ViewHolder.CHILD_INDEX_ICON));
+          }
+        }
+      };
+
   private BubbleExpansionStateListener bubbleExpansionStateListener;
 
   @Retention(RetentionPolicy.SOURCE)
@@ -163,13 +178,13 @@
   /** Creates instances of Bubble. The default implementation just calls the constructor. */
   @VisibleForTesting
   public interface BubbleFactory {
-    Bubble createBubble(@NonNull Context context);
+    Bubble createBubble(@NonNull Context context, @NonNull Handler handler);
   }
 
   private static BubbleFactory bubbleFactory = Bubble::new;
 
   public static Bubble createBubble(@NonNull Context context, @NonNull BubbleInfo info) {
-    Bubble bubble = bubbleFactory.createBubble(context);
+    Bubble bubble = bubbleFactory.createBubble(context, new Handler());
     bubble.setBubbleInfo(info);
     return bubble;
   }
@@ -185,14 +200,55 @@
   }
 
   @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-  Bubble(@NonNull Context context) {
+  Bubble(@NonNull Context context, @NonNull Handler handler) {
     context = new ContextThemeWrapper(context, R.style.Theme_AppCompat);
     this.context = context;
+    this.handler = handler;
     windowManager = context.getSystemService(WindowManager.class);
 
     viewHolder = new ViewHolder(context);
   }
 
+  /** Expands the main bubble menu. */
+  public void expand() {
+    if (expanded || textShowing || currentInfo.getActions().isEmpty()) {
+      try {
+        currentInfo.getPrimaryIntent().send();
+      } catch (CanceledException e) {
+        throw new RuntimeException(e);
+      }
+      return;
+    }
+
+    if (bubbleExpansionStateListener != null) {
+      bubbleExpansionStateListener.onBubbleExpansionStateChanged(ExpansionState.START_EXPANDING);
+    }
+    doResize(
+        () -> {
+          onLeftRightSwitch(isDrawingFromRight());
+          viewHolder.setDrawerVisibility(View.VISIBLE);
+        });
+    View expandedView = viewHolder.getExpandedView();
+    expandedView
+        .getViewTreeObserver()
+        .addOnPreDrawListener(
+            new OnPreDrawListener() {
+              @Override
+              public boolean onPreDraw() {
+                expandedView.getViewTreeObserver().removeOnPreDrawListener(this);
+                expandedView.setTranslationX(
+                    isDrawingFromRight() ? expandedView.getWidth() : -expandedView.getWidth());
+                expandedView
+                    .animate()
+                    .setInterpolator(new LinearOutSlowInInterpolator())
+                    .translationX(0);
+                return false;
+              }
+            });
+    setFocused(true);
+    expanded = true;
+  }
+
   /**
    * Make the bubble visible. Will show a short entrance animation as it enters. If the bubble is
    * already showing this method does nothing.
@@ -249,7 +305,15 @@
         .setInterpolator(new OvershootInterpolator())
         .scaleX(1)
         .scaleY(1)
-        .withEndAction(() -> visibility = Visibility.SHOWING)
+        .withEndAction(
+            () -> {
+              visibility = Visibility.SHOWING;
+              // Show the queued up text, if available.
+              if (textAfterShow != null) {
+                showText(textAfterShow);
+                textAfterShow = null;
+              }
+            })
         .start();
 
     updatePrimaryIconAnimation();
@@ -325,6 +389,12 @@
       transition.addTarget(startValues.view);
       transition.captureStartValues(startValues);
 
+      // If our view is not laid out yet, postpone showing the text.
+      if (startValues.values.isEmpty()) {
+        textAfterShow = text;
+        return;
+      }
+
       doResize(
           () -> {
             doShowText(text);
@@ -371,19 +441,8 @@
                     });
           });
     }
-    handler.removeCallbacks(null);
-    handler.postDelayed(
-        () -> {
-          textShowing = false;
-          if (hideAfterText) {
-            // Always reset here since text shouldn't keep showing.
-            hideAndReset();
-          } else {
-            doResize(
-                () -> viewHolder.getPrimaryButton().setDisplayedChild(ViewHolder.CHILD_INDEX_ICON));
-          }
-        },
-        SHOW_TEXT_DURATION_MILLIS);
+    handler.removeCallbacks(collapseRunnable);
+    handler.postDelayed(collapseRunnable, SHOW_TEXT_DURATION_MILLIS);
   }
 
   public void setBubbleExpansionStateListener(
@@ -415,42 +474,7 @@
   }
 
   void primaryButtonClick() {
-    if (expanded || textShowing || currentInfo.getActions().isEmpty()) {
-      try {
-        currentInfo.getPrimaryIntent().send();
-      } catch (CanceledException e) {
-        throw new RuntimeException(e);
-      }
-      return;
-    }
-
-    if (bubbleExpansionStateListener != null) {
-      bubbleExpansionStateListener.onBubbleExpansionStateChanged(ExpansionState.START_EXPANDING);
-    }
-    doResize(
-        () -> {
-          onLeftRightSwitch(isDrawingFromRight());
-          viewHolder.setDrawerVisibility(View.VISIBLE);
-        });
-    View expandedView = viewHolder.getExpandedView();
-    expandedView
-        .getViewTreeObserver()
-        .addOnPreDrawListener(
-            new OnPreDrawListener() {
-              @Override
-              public boolean onPreDraw() {
-                expandedView.getViewTreeObserver().removeOnPreDrawListener(this);
-                expandedView.setTranslationX(
-                    isDrawingFromRight() ? expandedView.getWidth() : -expandedView.getWidth());
-                expandedView
-                    .animate()
-                    .setInterpolator(new LinearOutSlowInInterpolator())
-                    .translationX(0);
-                return false;
-              }
-            });
-    setFocused(true);
-    expanded = true;
+    expand();
   }
 
   void onLeftRightSwitch(boolean onRight) {
diff --git a/java/com/android/dialershared/bubble/BubbleInfo.java b/java/com/android/bubble/BubbleInfo.java
similarity index 98%
rename from java/com/android/dialershared/bubble/BubbleInfo.java
rename to java/com/android/bubble/BubbleInfo.java
index eb9abd0..b4f81b3 100644
--- a/java/com/android/dialershared/bubble/BubbleInfo.java
+++ b/java/com/android/bubble/BubbleInfo.java
@@ -14,7 +14,7 @@
  * limitations under the License
  */
 
-package com.android.dialershared.bubble;
+package com.android.bubble;
 
 import android.app.PendingIntent;
 import android.graphics.drawable.Icon;
diff --git a/java/com/android/dialershared/bubble/ChangeOnScreenBounds.java b/java/com/android/bubble/ChangeOnScreenBounds.java
similarity index 99%
rename from java/com/android/dialershared/bubble/ChangeOnScreenBounds.java
rename to java/com/android/bubble/ChangeOnScreenBounds.java
index 8cd61af..0a7adf6 100644
--- a/java/com/android/dialershared/bubble/ChangeOnScreenBounds.java
+++ b/java/com/android/bubble/ChangeOnScreenBounds.java
@@ -14,7 +14,7 @@
  * limitations under the License
  */
 
-package com.android.dialershared.bubble;
+package com.android.bubble;
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
diff --git a/java/com/android/dialershared/bubble/CheckableImageButton.java b/java/com/android/bubble/CheckableImageButton.java
similarity index 98%
rename from java/com/android/dialershared/bubble/CheckableImageButton.java
rename to java/com/android/bubble/CheckableImageButton.java
index 7a5a432..dd9acce 100644
--- a/java/com/android/dialershared/bubble/CheckableImageButton.java
+++ b/java/com/android/bubble/CheckableImageButton.java
@@ -14,7 +14,7 @@
  * limitations under the License
  */
 
-package com.android.dialershared.bubble;
+package com.android.bubble;
 
 import android.content.Context;
 import android.support.v4.view.AccessibilityDelegateCompat;
diff --git a/java/com/android/dialershared/bubble/MoveHandler.java b/java/com/android/bubble/MoveHandler.java
similarity index 99%
rename from java/com/android/dialershared/bubble/MoveHandler.java
rename to java/com/android/bubble/MoveHandler.java
index 33507ef..06efbd4 100644
--- a/java/com/android/dialershared/bubble/MoveHandler.java
+++ b/java/com/android/bubble/MoveHandler.java
@@ -14,7 +14,7 @@
  * limitations under the License
  */
 
-package com.android.dialershared.bubble;
+package com.android.bubble;
 
 import android.content.Context;
 import android.graphics.Point;
diff --git a/java/com/android/dialershared/bubble/WindowRoot.java b/java/com/android/bubble/WindowRoot.java
similarity index 98%
rename from java/com/android/dialershared/bubble/WindowRoot.java
rename to java/com/android/bubble/WindowRoot.java
index 81d6b48..b9024c4 100644
--- a/java/com/android/dialershared/bubble/WindowRoot.java
+++ b/java/com/android/bubble/WindowRoot.java
@@ -14,7 +14,7 @@
  * limitations under the License
  */
 
-package com.android.dialershared.bubble;
+package com.android.bubble;
 
 import android.content.Context;
 import android.content.res.Configuration;
diff --git a/java/com/android/dialershared/bubble/res/color/bubble_checkable_mask.xml b/java/com/android/bubble/res/color/bubble_checkable_mask.xml
similarity index 100%
rename from java/com/android/dialershared/bubble/res/color/bubble_checkable_mask.xml
rename to java/com/android/bubble/res/color/bubble_checkable_mask.xml
diff --git a/java/com/android/dialershared/bubble/res/color/bubble_icon_tint_states.xml b/java/com/android/bubble/res/color/bubble_icon_tint_states.xml
similarity index 100%
rename from java/com/android/dialershared/bubble/res/color/bubble_icon_tint_states.xml
rename to java/com/android/bubble/res/color/bubble_icon_tint_states.xml
diff --git a/java/com/android/dialershared/bubble/res/drawable/bubble_background_pill_ltr.xml b/java/com/android/bubble/res/drawable/bubble_background_pill_ltr.xml
similarity index 100%
rename from java/com/android/dialershared/bubble/res/drawable/bubble_background_pill_ltr.xml
rename to java/com/android/bubble/res/drawable/bubble_background_pill_ltr.xml
diff --git a/java/com/android/dialershared/bubble/res/drawable/bubble_background_pill_rtl.xml b/java/com/android/bubble/res/drawable/bubble_background_pill_rtl.xml
similarity index 100%
rename from java/com/android/dialershared/bubble/res/drawable/bubble_background_pill_rtl.xml
rename to java/com/android/bubble/res/drawable/bubble_background_pill_rtl.xml
diff --git a/java/com/android/dialershared/bubble/res/drawable/bubble_ripple_checkable_circle.xml b/java/com/android/bubble/res/drawable/bubble_ripple_checkable_circle.xml
similarity index 100%
rename from java/com/android/dialershared/bubble/res/drawable/bubble_ripple_checkable_circle.xml
rename to java/com/android/bubble/res/drawable/bubble_ripple_checkable_circle.xml
diff --git a/java/com/android/dialershared/bubble/res/drawable/bubble_ripple_circle.xml b/java/com/android/bubble/res/drawable/bubble_ripple_circle.xml
similarity index 100%
rename from java/com/android/dialershared/bubble/res/drawable/bubble_ripple_circle.xml
rename to java/com/android/bubble/res/drawable/bubble_ripple_circle.xml
diff --git a/java/com/android/dialershared/bubble/res/layout/bubble_base.xml b/java/com/android/bubble/res/layout/bubble_base.xml
similarity index 96%
rename from java/com/android/dialershared/bubble/res/layout/bubble_base.xml
rename to java/com/android/bubble/res/layout/bubble_base.xml
index 76970f0..3b5735c 100644
--- a/java/com/android/dialershared/bubble/res/layout/bubble_base.xml
+++ b/java/com/android/bubble/res/layout/bubble_base.xml
@@ -54,7 +54,7 @@
         android:visibility="gone"
         tools:backgroundTint="#FF0000FF"
         tools:visibility="visible">
-      <com.android.dialershared.bubble.CheckableImageButton
+      <com.android.bubble.CheckableImageButton
           android:id="@+id/bubble_icon_first"
           android:layout_width="@dimen/bubble_size"
           android:layout_height="@dimen/bubble_size"
@@ -64,7 +64,7 @@
           android:tintMode="src_in"
           tools:background="@drawable/bubble_ripple_checkable_circle"
           tools:src="@android:drawable/ic_lock_idle_lock"/>
-      <com.android.dialershared.bubble.CheckableImageButton
+      <com.android.bubble.CheckableImageButton
           android:id="@+id/bubble_icon_second"
           android:layout_width="@dimen/bubble_size"
           android:layout_height="@dimen/bubble_size"
@@ -74,7 +74,7 @@
           android:tintMode="src_in"
           tools:background="@drawable/bubble_ripple_checkable_circle"
           tools:src="@android:drawable/ic_input_add"/>
-      <com.android.dialershared.bubble.CheckableImageButton
+      <com.android.bubble.CheckableImageButton
           android:id="@+id/bubble_icon_third"
           android:layout_width="@dimen/bubble_size"
           android:layout_height="@dimen/bubble_size"
diff --git a/java/com/android/dialershared/bubble/res/values/colors.xml b/java/com/android/bubble/res/values/colors.xml
similarity index 100%
rename from java/com/android/dialershared/bubble/res/values/colors.xml
rename to java/com/android/bubble/res/values/colors.xml
diff --git a/java/com/android/dialershared/bubble/res/values/values.xml b/java/com/android/bubble/res/values/values.xml
similarity index 100%
rename from java/com/android/dialershared/bubble/res/values/values.xml
rename to java/com/android/bubble/res/values/values.xml
diff --git a/java/com/android/dialer/app/AndroidManifest.xml b/java/com/android/dialer/app/AndroidManifest.xml
index 1c04b76..2ef5dad 100644
--- a/java/com/android/dialer/app/AndroidManifest.xml
+++ b/java/com/android/dialer/app/AndroidManifest.xml
@@ -61,6 +61,18 @@
   <application android:theme="@style/Theme.AppCompat">
 
     <activity
+      android:exported="false"
+      android:label="@string/manage_blocked_numbers_label"
+      android:name="com.android.dialer.app.filterednumber.BlockedNumbersSettingsActivity"
+      android:parentActivityName="com.android.dialer.app.settings.DialerSettingsActivity"
+      android:theme="@style/ManageBlockedNumbersStyle">
+      <intent-filter>
+        <action android:name="com.android.dialer.action.BLOCKED_NUMBERS_SETTINGS"/>
+        <category android:name="android.intent.category.DEFAULT"/>
+      </intent-filter>
+    </activity>
+
+    <activity
       android:label="@string/call_log_activity_title"
       android:name="com.android.dialer.app.calllog.CallLogActivity"
       android:theme="@style/DialtactsThemeWithoutActionBarOverlay">
@@ -91,6 +103,11 @@
       android:name="com.android.dialer.app.calllog.CallLogNotificationsService"
       />
 
+    <service
+      android:name="com.android.dialer.app.calllog.VoicemailNotificationJobService"
+      android:permission="android.permission.BIND_JOB_SERVICE"
+      />
+
     <receiver
       android:directBootAware="true"
       android:name="com.android.dialer.app.calllog.MissedCallNotificationReceiver">
diff --git a/java/com/android/dialer/app/DialtactsActivity.java b/java/com/android/dialer/app/DialtactsActivity.java
index 7f5a9b9..ec67f54 100644
--- a/java/com/android/dialer/app/DialtactsActivity.java
+++ b/java/com/android/dialer/app/DialtactsActivity.java
@@ -695,10 +695,16 @@
     int resId = view.getId();
     if (resId == R.id.floating_action_button) {
       if (!mIsDialpadShown) {
+        LogUtil.i(
+            "DialtactsActivity.onClick", "floating action button clicked, going to show dialpad");
         PerformanceReport.recordClick(UiAction.Type.OPEN_DIALPAD);
         mInCallDialpadUp = false;
         showDialpadFragment(true);
         PostCall.closePrompt();
+      } else {
+        LogUtil.i(
+            "DialtactsActivity.onClick",
+            "floating action button clicked, but dialpad is already showing");
       }
     } else if (resId == R.id.voice_search_button) {
       try {
diff --git a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
index 60ed7dd..ef6236b 100644
--- a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
+++ b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
@@ -781,19 +781,28 @@
     View transcriptContainerView = phoneCallDetailsViews.transcriptionView;
     TextView transcriptView = phoneCallDetailsViews.voicemailTranscriptionView;
     TextView transcriptBrandingView = phoneCallDetailsViews.voicemailTranscriptionBrandingView;
-    if (TextUtils.isEmpty(transcriptView.getText())) {
-      Assert.checkArgument(TextUtils.isEmpty(transcriptBrandingView.getText()));
-    }
-    if (!isExpanded || TextUtils.isEmpty(transcriptView.getText())) {
+    if (!isExpanded) {
       transcriptContainerView.setVisibility(View.GONE);
       return;
     }
-    transcriptContainerView.setVisibility(View.VISIBLE);
-    transcriptView.setVisibility(View.VISIBLE);
-    if (TextUtils.isEmpty(transcriptBrandingView.getText())) {
-      phoneCallDetailsViews.voicemailTranscriptionBrandingView.setVisibility(View.GONE);
+
+    boolean show = false;
+    if (TextUtils.isEmpty(transcriptView.getText())) {
+      transcriptView.setVisibility(View.GONE);
     } else {
-      phoneCallDetailsViews.voicemailTranscriptionBrandingView.setVisibility(View.VISIBLE);
+      transcriptView.setVisibility(View.VISIBLE);
+      show = true;
+    }
+    if (TextUtils.isEmpty(transcriptBrandingView.getText())) {
+      transcriptBrandingView.setVisibility(View.GONE);
+    } else {
+      transcriptBrandingView.setVisibility(View.VISIBLE);
+      show = true;
+    }
+    if (show) {
+      transcriptContainerView.setVisibility(View.VISIBLE);
+    } else {
+      transcriptContainerView.setVisibility(View.GONE);
     }
   }
 
diff --git a/java/com/android/dialer/app/calllog/CallLogNotificationsQueryHelper.java b/java/com/android/dialer/app/calllog/CallLogNotificationsQueryHelper.java
index 43e03e9..2f8b1f4 100644
--- a/java/com/android/dialer/app/calllog/CallLogNotificationsQueryHelper.java
+++ b/java/com/android/dialer/app/calllog/CallLogNotificationsQueryHelper.java
@@ -24,7 +24,7 @@
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
-import android.os.Build.VERSION_CODES;
+import android.os.Build;
 import android.provider.CallLog.Calls;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
@@ -35,14 +35,17 @@
 import com.android.dialer.app.R;
 import com.android.dialer.calllogutils.PhoneNumberDisplayUtil;
 import com.android.dialer.common.LogUtil;
+import com.android.dialer.compat.android.provider.VoicemailCompat;
 import com.android.dialer.location.GeoUtil;
 import com.android.dialer.phonenumbercache.ContactInfo;
 import com.android.dialer.phonenumbercache.ContactInfoHelper;
 import com.android.dialer.util.PermissionsUtil;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 /** Helper class operating on call log notifications. */
+@TargetApi(Build.VERSION_CODES.M)
 public class CallLogNotificationsQueryHelper {
 
   private final Context mContext;
@@ -133,6 +136,10 @@
     return new DefaultNewCallsQuery(context.getApplicationContext(), contentResolver);
   }
 
+  NewCallsQuery getNewCallsQuery() {
+    return mNewCallsQuery;
+  }
+
   /**
    * Get all voicemails with the "new" flag set to 1.
    *
@@ -216,6 +223,10 @@
     /** Returns the new calls of a certain type for which a notification should be generated. */
     @Nullable
     List<NewCall> query(int type);
+
+    /** Returns a {@link NewCall} pointed by the {@code callsUri} */
+    @Nullable
+    NewCall query(Uri callsUri);
   }
 
   /** Information about a new voicemail. */
@@ -230,6 +241,7 @@
     public final String transcription;
     public final String countryIso;
     public final long dateMs;
+    public final int transcriptionState;
 
     public NewCall(
         Uri callsUri,
@@ -240,7 +252,8 @@
         String accountId,
         String transcription,
         String countryIso,
-        long dateMs) {
+        long dateMs,
+        int transcriptionState) {
       this.callsUri = callsUri;
       this.voicemailUri = voicemailUri;
       this.number = number;
@@ -250,6 +263,7 @@
       this.transcription = transcription;
       this.countryIso = countryIso;
       this.dateMs = dateMs;
+      this.transcriptionState = transcriptionState;
     }
   }
 
@@ -270,6 +284,16 @@
       Calls.COUNTRY_ISO,
       Calls.DATE
     };
+
+    private static final String[] PROJECTION_O;
+
+    static {
+      List<String> list = new ArrayList<>();
+      list.addAll(Arrays.asList(PROJECTION));
+      list.add(VoicemailCompat.TRANSCRIPTION_STATE);
+      PROJECTION_O = list.toArray(new String[list.size()]);
+    }
+
     private static final int ID_COLUMN_INDEX = 0;
     private static final int NUMBER_COLUMN_INDEX = 1;
     private static final int VOICEMAIL_URI_COLUMN_INDEX = 2;
@@ -279,6 +303,7 @@
     private static final int TRANSCRIPTION_COLUMN_INDEX = 6;
     private static final int COUNTRY_ISO_COLUMN_INDEX = 7;
     private static final int DATE_COLUMN_INDEX = 8;
+    private static final int TRANSCRIPTION_STATE_COLUMN_INDEX = 9;
 
     private final ContentResolver mContentResolver;
     private final Context mContext;
@@ -290,7 +315,7 @@
 
     @Override
     @Nullable
-    @TargetApi(VERSION_CODES.M)
+    @TargetApi(Build.VERSION_CODES.M)
     public List<NewCall> query(int type) {
       if (!PermissionsUtil.hasPermission(mContext, Manifest.permission.READ_CALL_LOG)) {
         LogUtil.w(
@@ -298,12 +323,18 @@
             "no READ_CALL_LOG permission, returning null for calls lookup.");
         return null;
       }
-      final String selection = String.format("%s = 1 AND %s = ?", Calls.NEW, Calls.TYPE);
+      // A call is "new" when:
+      // NEW is 1. usually set when a new row is inserted
+      // TYPE matches the query type.
+      // IS_READ is not 1. A call might be backed up and restored, so it will be "new" to the
+      //   call log, but the user has already read it on another device.
+      final String selection =
+          String.format("%s = 1 AND %s = ? AND %s IS NOT 1", Calls.NEW, Calls.TYPE, Calls.IS_READ);
       final String[] selectionArgs = new String[] {Integer.toString(type)};
       try (Cursor cursor =
           mContentResolver.query(
               Calls.CONTENT_URI_WITH_VOICEMAIL,
-              PROJECTION,
+              (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) ? PROJECTION_O : PROJECTION,
               selection,
               selectionArgs,
               Calls.DEFAULT_SORT_ORDER)) {
@@ -323,6 +354,26 @@
       }
     }
 
+    @Nullable
+    @Override
+    public NewCall query(Uri callsUri) {
+      if (!PermissionsUtil.hasPermission(mContext, Manifest.permission.READ_CALL_LOG)) {
+        LogUtil.w(
+            "CallLogNotificationsQueryHelper.DefaultNewCallsQuery.query",
+            "No READ_CALL_LOG permission, returning null for calls lookup.");
+        return null;
+      }
+      try (Cursor cursor = mContentResolver.query(callsUri, PROJECTION, null, null, null)) {
+        if (cursor == null) {
+          return null;
+        }
+        if (!cursor.moveToFirst()) {
+          return null;
+        }
+        return createNewCallsFromCursor(cursor);
+      }
+    }
+
     /** Returns an instance of {@link NewCall} created by using the values of the cursor. */
     private NewCall createNewCallsFromCursor(Cursor cursor) {
       String voicemailUriString = cursor.getString(VOICEMAIL_URI_COLUMN_INDEX);
@@ -339,7 +390,10 @@
           cursor.getString(PHONE_ACCOUNT_ID_COLUMN_INDEX),
           cursor.getString(TRANSCRIPTION_COLUMN_INDEX),
           cursor.getString(COUNTRY_ISO_COLUMN_INDEX),
-          cursor.getLong(DATE_COLUMN_INDEX));
+          cursor.getLong(DATE_COLUMN_INDEX),
+          Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
+              ? cursor.getInt(TRANSCRIPTION_STATE_COLUMN_INDEX)
+              : VoicemailCompat.TRANSCRIPTION_NOT_STARTED);
     }
   }
 }
diff --git a/java/com/android/dialer/app/calllog/MissedCallNotifier.java b/java/com/android/dialer/app/calllog/MissedCallNotifier.java
index b363b5a..de76619 100644
--- a/java/com/android/dialer/app/calllog/MissedCallNotifier.java
+++ b/java/com/android/dialer/app/calllog/MissedCallNotifier.java
@@ -47,6 +47,7 @@
 import com.android.dialer.callintent.CallIntentBuilder;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.common.concurrent.DialerExecutor.Worker;
+import com.android.dialer.compat.android.provider.VoicemailCompat;
 import com.android.dialer.notification.DialerNotificationManager;
 import com.android.dialer.notification.NotificationChannelId;
 import com.android.dialer.notification.NotificationManagerUtils;
@@ -153,7 +154,8 @@
                   null,
                   null,
                   null,
-                  System.currentTimeMillis());
+                  System.currentTimeMillis(),
+                  VoicemailCompat.TRANSCRIPTION_NOT_STARTED);
 
       // TODO: look up caller ID that is not in contacts.
       ContactInfo contactInfo =
diff --git a/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java b/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java
index a6e8f10..189279e 100644
--- a/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java
+++ b/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java
@@ -146,31 +146,41 @@
     if (isVoicemail) {
       int relevantLinkTypes = Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS | Linkify.WEB_URLS;
       views.voicemailTranscriptionView.setAutoLinkMask(relevantLinkTypes);
-      boolean showTranscriptBranding = false;
+
+      String transcript = "";
+      String branding = "";
       if (!TextUtils.isEmpty(details.transcription)) {
-        views.voicemailTranscriptionView.setText(details.transcription);
+        transcript = details.transcription;
 
         // Set the branding text if the voicemail was transcribed by google
         // TODO(mdooley): the transcription state is only set by the google transcription code,
         // but a better solution would be to check the SOURCE_PACKAGE
-        showTranscriptBranding =
-            details.transcriptionState == VoicemailCompat.TRANSCRIPTION_AVAILABLE;
+        if (details.transcriptionState == VoicemailCompat.TRANSCRIPTION_AVAILABLE) {
+          branding = mResources.getString(R.string.voicemail_transcription_branding_text);
+        }
       } else {
-        if (details.transcriptionState == VoicemailCompat.TRANSCRIPTION_IN_PROGRESS) {
-          views.voicemailTranscriptionView.setText(
-              mResources.getString(R.string.voicemail_transcription_in_progress));
-        } else if (details.transcriptionState == VoicemailCompat.TRANSCRIPTION_FAILED) {
-          views.voicemailTranscriptionView.setText(
-              mResources.getString(R.string.voicemail_transcription_failed));
+        switch (details.transcriptionState) {
+          case VoicemailCompat.TRANSCRIPTION_IN_PROGRESS:
+            branding = mResources.getString(R.string.voicemail_transcription_in_progress);
+            break;
+          case VoicemailCompat.TRANSCRIPTION_FAILED_NO_SPEECH_DETECTED:
+            branding = mResources.getString(R.string.voicemail_transcription_failed_no_speech);
+            break;
+          case VoicemailCompat.TRANSCRIPTION_FAILED_LANGUAGE_NOT_SUPPORTED:
+            branding =
+                mResources.getString(
+                    R.string.voicemail_transcription_failed_language_not_supported);
+            break;
+          case VoicemailCompat.TRANSCRIPTION_FAILED:
+            branding = mResources.getString(R.string.voicemail_transcription_failed);
+            break;
+          default:
+            break; // Fall through
         }
       }
 
-      if (showTranscriptBranding) {
-        views.voicemailTranscriptionBrandingView.setText(
-            mResources.getString(R.string.voicemail_transcription_branding_text));
-      } else {
-        views.voicemailTranscriptionBrandingView.setText("");
-      }
+      views.voicemailTranscriptionView.setText(transcript);
+      views.voicemailTranscriptionBrandingView.setText(branding);
     }
 
     // Bold if not read
diff --git a/java/com/android/dialer/app/calllog/VisualVoicemailNotifier.java b/java/com/android/dialer/app/calllog/VisualVoicemailNotifier.java
index cbadfd3..ceae3d3 100644
--- a/java/com/android/dialer/app/calllog/VisualVoicemailNotifier.java
+++ b/java/com/android/dialer/app/calllog/VisualVoicemailNotifier.java
@@ -27,7 +27,6 @@
 import android.os.Build.VERSION_CODES;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
-import android.support.v4.os.BuildCompat;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
 import android.telephony.TelephonyManager;
@@ -39,6 +38,7 @@
 import com.android.dialer.app.contactinfo.ContactPhotoLoader;
 import com.android.dialer.app.list.DialtactsPagerAdapter;
 import com.android.dialer.common.LogUtil;
+import com.android.dialer.compat.android.provider.VoicemailCompat;
 import com.android.dialer.logging.DialerImpression;
 import com.android.dialer.logging.Logger;
 import com.android.dialer.notification.DialerNotificationManager;
@@ -54,9 +54,9 @@
   /** Prefix used to generate a unique tag for each voicemail notification. */
   private static final String NOTIFICATION_TAG_PREFIX = "VisualVoicemail_";
   /** Common ID for all voicemail notifications. */
-  private static final int NOTIFICATION_ID = 1;
+  static final int NOTIFICATION_ID = 1;
   /** Tag for the group summary notification. */
-  private static final String GROUP_SUMMARY_NOTIFICATION_TAG = "GroupSummary_VisualVoicemail";
+  static final String GROUP_SUMMARY_NOTIFICATION_TAG = "GroupSummary_VisualVoicemail";
   /**
    * Key used to associate all voicemail notifications and the summary as belonging to a single
    * group.
@@ -84,7 +84,7 @@
             .setGroupSummary(true)
             .setContentIntent(newVoicemailIntent(context, null));
 
-    if (BuildCompat.isAtLeastO()) {
+    if (VERSION.SDK_INT >= VERSION_CODES.O) {
       groupSummary.setGroupAlertBehavior(Notification.GROUP_ALERT_CHILDREN);
       PhoneAccountHandle handle = getAccountForCall(context, newCalls.get(0));
       groupSummary.setChannelId(NotificationChannelManager.getVoicemailChannelId(context, handle));
@@ -136,7 +136,7 @@
         .setAutoCancel(true);
   }
 
-  private static Notification createNotificationForVoicemail(
+  static Notification createNotificationForVoicemail(
       @NonNull Context context,
       @NonNull NewCall voicemail,
       @NonNull Map<String, ContactInfo> contactInfos) {
@@ -146,10 +146,6 @@
     Notification.Builder builder =
         createNotificationBuilder(context)
             .setContentTitle(
-                context
-                    .getResources()
-                    .getQuantityString(R.plurals.notification_voicemail_title, 1, 1))
-            .setContentText(
                 ContactDisplayUtils.getTtsSpannedPhoneNumber(
                     context.getResources(),
                     R.string.notification_new_voicemail_ticker,
@@ -158,13 +154,51 @@
             .setSound(getVoicemailRingtoneUri(context, handle))
             .setDefaults(getNotificationDefaultFlags(context, handle));
 
+    if (!TextUtils.isEmpty(voicemail.transcription)) {
+      Logger.get(context)
+          .logImpression(DialerImpression.Type.VVM_NOTIFICATION_CREATED_WITH_TRANSCRIPTION);
+      builder.setContentText(voicemail.transcription);
+    } else {
+      switch (voicemail.transcriptionState) {
+        case VoicemailCompat.TRANSCRIPTION_IN_PROGRESS:
+          Logger.get(context)
+              .logImpression(DialerImpression.Type.VVM_NOTIFICATION_CREATED_WITH_IN_PROGRESS);
+          builder.setContentText(context.getString(R.string.voicemail_transcription_in_progress));
+          break;
+        case VoicemailCompat.TRANSCRIPTION_FAILED_NO_SPEECH_DETECTED:
+          Logger.get(context)
+              .logImpression(
+                  DialerImpression.Type.VVM_NOTIFICATION_CREATED_WITH_TRANSCRIPTION_FAILURE);
+          builder.setContentText(
+              context.getString(R.string.voicemail_transcription_failed_no_speech));
+          break;
+        case VoicemailCompat.TRANSCRIPTION_FAILED_LANGUAGE_NOT_SUPPORTED:
+          Logger.get(context)
+              .logImpression(
+                  DialerImpression.Type.VVM_NOTIFICATION_CREATED_WITH_TRANSCRIPTION_FAILURE);
+          builder.setContentText(
+              context.getString(R.string.voicemail_transcription_failed_language_not_supported));
+          break;
+        case VoicemailCompat.TRANSCRIPTION_FAILED:
+          Logger.get(context)
+              .logImpression(
+                  DialerImpression.Type.VVM_NOTIFICATION_CREATED_WITH_TRANSCRIPTION_FAILURE);
+          builder.setContentText(context.getString(R.string.voicemail_transcription_failed));
+          break;
+        default:
+          Logger.get(context)
+              .logImpression(DialerImpression.Type.VVM_NOTIFICATION_CREATED_WITH_NO_TRANSCRIPTION);
+          break;
+      }
+    }
+
     if (voicemail.voicemailUri != null) {
       builder.setDeleteIntent(
           CallLogNotificationsService.createMarkSingleNewVoicemailAsOldIntent(
               context, voicemail.voicemailUri));
     }
 
-    if (BuildCompat.isAtLeastO()) {
+    if (VERSION.SDK_INT >= VERSION_CODES.O) {
       builder.setChannelId(NotificationChannelManager.getVoicemailChannelId(context, handle));
     }
 
@@ -173,11 +207,6 @@
     if (photoIcon != null) {
       builder.setLargeIcon(photoIcon);
     }
-    if (!TextUtils.isEmpty(voicemail.transcription)) {
-      Logger.get(context)
-          .logImpression(DialerImpression.Type.VVM_NOTIFICATION_CREATED_WITH_TRANSCRIPTION);
-      builder.setStyle(new Notification.BigTextStyle().bigText(voicemail.transcription));
-    }
     builder.setContentIntent(newVoicemailIntent(context, voicemail));
     Logger.get(context).logImpression(DialerImpression.Type.VVM_NOTIFICATION_CREATED);
     return builder.build();
diff --git a/java/com/android/dialer/app/calllog/VisualVoicemailUpdateTask.java b/java/com/android/dialer/app/calllog/VisualVoicemailUpdateTask.java
index d6601be..219ad67 100644
--- a/java/com/android/dialer/app/calllog/VisualVoicemailUpdateTask.java
+++ b/java/com/android/dialer/app/calllog/VisualVoicemailUpdateTask.java
@@ -17,6 +17,8 @@
 package com.android.dialer.app.calllog;
 
 import android.content.Context;
+import android.net.Uri;
+import android.service.notification.StatusBarNotification;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.annotation.WorkerThread;
@@ -30,6 +32,7 @@
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.common.concurrent.DialerExecutor.Worker;
 import com.android.dialer.common.concurrent.DialerExecutors;
+import com.android.dialer.notification.DialerNotificationManager;
 import com.android.dialer.phonenumbercache.ContactInfo;
 import com.android.dialer.telecom.TelecomUtil;
 import java.util.ArrayList;
@@ -57,13 +60,20 @@
       CallLogNotificationsQueryHelper queryHelper,
       FilteredNumberAsyncQueryHandler queryHandler) {
     Assert.isWorkerThread();
+    LogUtil.enterBlock("VisualVoicemailUpdateTask.updateNotification");
 
-    List<NewCall> newCalls = queryHelper.getNewVoicemails();
-    if (newCalls == null) {
+    List<NewCall> voicemailsToNotify = queryHelper.getNewVoicemails();
+    if (voicemailsToNotify == null) {
+      // Query failed, just return
       return;
     }
-    newCalls = filterBlockedNumbers(context, queryHandler, newCalls);
-    if (newCalls.isEmpty()) {
+
+    voicemailsToNotify.addAll(getAndUpdateVoicemailsWithExistingNotification(context, queryHelper));
+    voicemailsToNotify = filterBlockedNumbers(context, queryHandler, voicemailsToNotify);
+    if (voicemailsToNotify.isEmpty()) {
+      LogUtil.i("VisualVoicemailUpdateTask.updateNotification", "no voicemails to notify about");
+      VisualVoicemailNotifier.cancelAllVoicemailNotifications(context);
+      VoicemailNotificationJobService.cancelJob(context);
       return;
     }
 
@@ -73,7 +83,7 @@
     // Maps each number into a name: if a number is in the map, it has already left a more
     // recent voicemail.
     Map<String, ContactInfo> contactInfos = new ArrayMap<>();
-    for (NewCall newCall : newCalls) {
+    for (NewCall newCall : voicemailsToNotify) {
       if (!contactInfos.containsKey(newCall.number)) {
         ContactInfo contactInfo =
             queryHelper.getContactInfo(
@@ -90,7 +100,43 @@
         }
       }
     }
-    VisualVoicemailNotifier.showNotifications(context, newCalls, contactInfos, callers);
+    VisualVoicemailNotifier.showNotifications(context, voicemailsToNotify, contactInfos, callers);
+
+    // Set trigger to update notifications when database changes.
+    VoicemailNotificationJobService.scheduleJob(context);
+  }
+
+  /**
+   * Cancel notification for voicemail that is already deleted. Returns a list of voicemails that
+   * already has notifications posted and should be updated.
+   */
+  @WorkerThread
+  @NonNull
+  private static List<NewCall> getAndUpdateVoicemailsWithExistingNotification(
+      Context context, CallLogNotificationsQueryHelper queryHelper) {
+    Assert.isWorkerThread();
+    List<NewCall> result = new ArrayList<>();
+    for (StatusBarNotification notification :
+        DialerNotificationManager.getActiveNotifications(context)) {
+      if (notification.getId() != VisualVoicemailNotifier.NOTIFICATION_ID) {
+        continue;
+      }
+      if (TextUtils.equals(
+          notification.getTag(), VisualVoicemailNotifier.GROUP_SUMMARY_NOTIFICATION_TAG)) {
+        // Group header
+        continue;
+      }
+      NewCall existingCall = queryHelper.getNewCallsQuery().query(Uri.parse(notification.getTag()));
+      if (existingCall != null) {
+        result.add(existingCall);
+      } else {
+        LogUtil.i(
+            "VisualVoicemailUpdateTask.getVoicemailsWithExistingNotification",
+            "voicemail deleted, removing notification");
+        DialerNotificationManager.cancel(context, notification.getTag(), notification.getId());
+      }
+    }
+    return result;
   }
 
   @WorkerThread
diff --git a/java/com/android/dialer/app/calllog/VoicemailNotificationJobService.java b/java/com/android/dialer/app/calllog/VoicemailNotificationJobService.java
new file mode 100644
index 0000000..ba61601
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/VoicemailNotificationJobService.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.app.calllog;
+
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Build;
+import android.provider.VoicemailContract;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.constants.ScheduledJobIds;
+
+/** Monitors voicemail provider changes to update active notifications. */
+public class VoicemailNotificationJobService extends JobService {
+
+  private static JobInfo jobInfo;
+
+  /**
+   * Start monitoring the provider. The provider should be monitored whenever a visual voicemail
+   * notification is visible.
+   */
+  public static void scheduleJob(Context context) {
+    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+      LogUtil.i("VoicemailNotificationJobService.scheduleJob", "not supported");
+    } else {
+      context.getSystemService(JobScheduler.class).schedule(getJobInfo(context));
+      LogUtil.i("VoicemailNotificationJobService.scheduleJob", "job scheduled");
+    }
+  }
+
+  /**
+   * Stop monitoring the provider. The provider should not be monitored when visual voicemail
+   * notification is cleared.
+   */
+  public static void cancelJob(Context context) {
+    context.getSystemService(JobScheduler.class).cancel(ScheduledJobIds.VVM_NOTIFICATION_JOB);
+    LogUtil.i("VoicemailNotificationJobService.scheduleJob", "job canceled");
+  }
+
+  @Override
+  public boolean onStartJob(JobParameters params) {
+    LogUtil.i("VoicemailNotificationJobService.onStartJob", "updating notification");
+    VisualVoicemailUpdateTask.scheduleTask(
+        this,
+        () -> {
+          jobFinished(params, false);
+        });
+    return true; // Running in background
+  }
+
+  @Override
+  public boolean onStopJob(JobParameters params) {
+    return false;
+  }
+
+  private static JobInfo getJobInfo(Context context) {
+    if (jobInfo == null) {
+      jobInfo =
+          new JobInfo.Builder(
+                  ScheduledJobIds.VVM_NOTIFICATION_JOB,
+                  new ComponentName(context, VoicemailNotificationJobService.class))
+              .addTriggerContentUri(
+                  new JobInfo.TriggerContentUri(
+                      VoicemailContract.Voicemails.CONTENT_URI,
+                      JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS))
+              .setTriggerContentMaxDelay(0)
+              .build();
+    }
+
+    return jobInfo;
+  }
+}
diff --git a/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java b/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java
index 2fbebdd..169d0fd 100644
--- a/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java
+++ b/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java
@@ -45,7 +45,8 @@
   public static void markAllNewVoicemailsAsRead(final @NonNull Context context) {
     ThreadUtil.postOnUiThread(
         () -> {
-          new VoicemailQueryHandler(context.getContentResolver()).markNewVoicemailsAsOld(null);
+          new VoicemailQueryHandler(context.getContentResolver())
+              .markNewVoicemailsAsOld(context, null);
         });
   }
 
@@ -59,12 +60,12 @@
     ThreadUtil.postOnUiThread(
         () -> {
           new VoicemailQueryHandler(context.getContentResolver())
-              .markNewVoicemailsAsOld(voicemailUri);
+              .markNewVoicemailsAsOld(context, voicemailUri);
         });
   }
 
   /** Updates all new voicemails to mark them as old. */
-  private void markNewVoicemailsAsOld(@Nullable Uri voicemailUri) {
+  private void markNewVoicemailsAsOld(Context context, @Nullable Uri voicemailUri) {
     // Mark all "new" voicemails as not new anymore.
     StringBuilder where = new StringBuilder();
     where.append(Calls.NEW);
@@ -88,5 +89,8 @@
         voicemailUri == null
             ? new String[] {Integer.toString(Calls.VOICEMAIL_TYPE)}
             : new String[] {Integer.toString(Calls.VOICEMAIL_TYPE), voicemailUri.toString()});
+
+    // No more notifications, stop monitoring the voicemail provider
+    VoicemailNotificationJobService.cancelJob(context);
   }
 }
diff --git a/java/com/android/dialer/app/filterednumber/BlockedNumbersAdapter.java b/java/com/android/dialer/app/filterednumber/BlockedNumbersAdapter.java
new file mode 100644
index 0000000..4f8bc66
--- /dev/null
+++ b/java/com/android/dialer/app/filterednumber/BlockedNumbersAdapter.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.dialer.app.filterednumber;
+
+import android.app.FragmentManager;
+import android.content.Context;
+import android.database.Cursor;
+import android.telephony.PhoneNumberUtils;
+import android.view.View;
+import com.android.dialer.app.R;
+import com.android.dialer.blocking.BlockNumberDialogFragment;
+import com.android.dialer.contactphoto.ContactPhotoManager;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
+import com.android.dialer.location.GeoUtil;
+import com.android.dialer.logging.InteractionEvent;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.phonenumbercache.ContactInfoHelper;
+
+/** TODO(calderwoodra): documentation */
+public class BlockedNumbersAdapter extends NumbersAdapter {
+
+  private BlockedNumbersAdapter(
+      Context context,
+      FragmentManager fragmentManager,
+      ContactInfoHelper contactInfoHelper,
+      ContactPhotoManager contactPhotoManager) {
+    super(context, fragmentManager, contactInfoHelper, contactPhotoManager);
+  }
+
+  public static BlockedNumbersAdapter newBlockedNumbersAdapter(
+      Context context, FragmentManager fragmentManager) {
+    return new BlockedNumbersAdapter(
+        context,
+        fragmentManager,
+        new ContactInfoHelper(context, GeoUtil.getCurrentCountryIso(context)),
+        ContactPhotoManager.getInstance(context));
+  }
+
+  @Override
+  public void bindView(View view, final Context context, Cursor cursor) {
+    super.bindView(view, context, cursor);
+    final Integer id = cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns._ID));
+    final String countryIso =
+        cursor.getString(cursor.getColumnIndex(FilteredNumberColumns.COUNTRY_ISO));
+    final String number = cursor.getString(cursor.getColumnIndex(FilteredNumberColumns.NUMBER));
+
+    final View deleteButton = view.findViewById(R.id.delete_button);
+    deleteButton.setOnClickListener(
+        new View.OnClickListener() {
+          @Override
+          public void onClick(View view) {
+            BlockNumberDialogFragment.show(
+                id,
+                number,
+                countryIso,
+                PhoneNumberUtils.formatNumber(number, countryIso),
+                R.id.blocked_numbers_activity_container,
+                getFragmentManager(),
+                new BlockNumberDialogFragment.Callback() {
+                  @Override
+                  public void onFilterNumberSuccess() {}
+
+                  @Override
+                  public void onUnfilterNumberSuccess() {
+                    Logger.get(context)
+                        .logInteraction(InteractionEvent.Type.UNBLOCK_NUMBER_MANAGEMENT_SCREEN);
+                  }
+
+                  @Override
+                  public void onChangeFilteredNumberUndo() {}
+                });
+          }
+        });
+
+    updateView(view, number, countryIso);
+  }
+
+  @Override
+  public boolean isEmpty() {
+    // Always return false, so that the header with blocking-related options always shows.
+    return false;
+  }
+}
diff --git a/java/com/android/dialer/app/filterednumber/BlockedNumbersFragment.java b/java/com/android/dialer/app/filterednumber/BlockedNumbersFragment.java
new file mode 100644
index 0000000..36afe54
--- /dev/null
+++ b/java/com/android/dialer/app/filterednumber/BlockedNumbersFragment.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.dialer.app.filterednumber;
+
+import android.app.ListFragment;
+import android.app.LoaderManager;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.Loader;
+import android.database.Cursor;
+import android.graphics.drawable.ColorDrawable;
+import android.os.Bundle;
+import android.support.v4.app.ActivityCompat;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+import com.android.dialer.app.R;
+import com.android.dialer.blocking.BlockedNumbersMigrator;
+import com.android.dialer.blocking.BlockedNumbersMigrator.Listener;
+import com.android.dialer.blocking.FilteredNumberCompat;
+import com.android.dialer.blocking.FilteredNumbersUtil;
+import com.android.dialer.blocking.FilteredNumbersUtil.CheckForSendToVoicemailContactListener;
+import com.android.dialer.blocking.FilteredNumbersUtil.ImportSendToVoicemailContactsListener;
+import com.android.dialer.database.FilteredNumberContract;
+import com.android.dialer.lettertile.LetterTileDrawable;
+import com.android.dialer.voicemailstatus.VisualVoicemailEnabledChecker;
+
+/** TODO(calderwoodra): documentation */
+public class BlockedNumbersFragment extends ListFragment
+    implements LoaderManager.LoaderCallbacks<Cursor>,
+        View.OnClickListener,
+        VisualVoicemailEnabledChecker.Callback {
+
+  private static final char ADD_BLOCKED_NUMBER_ICON_LETTER = '+';
+  protected View migratePromoView;
+  private BlockedNumbersMigrator blockedNumbersMigratorForTest;
+  private TextView blockedNumbersText;
+  private TextView footerText;
+  private BlockedNumbersAdapter mAdapter;
+  private VisualVoicemailEnabledChecker mVoicemailEnabledChecker;
+  private View mImportSettings;
+  private View mBlockedNumbersDisabledForEmergency;
+  private View mBlockedNumberListDivider;
+
+  @Override
+  public Context getContext() {
+    return getActivity();
+  }
+
+  @Override
+  public void onActivityCreated(Bundle savedInstanceState) {
+    super.onActivityCreated(savedInstanceState);
+
+    LayoutInflater inflater =
+        (LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+    getListView().addHeaderView(inflater.inflate(R.layout.blocked_number_header, null));
+    getListView().addFooterView(inflater.inflate(R.layout.blocked_number_footer, null));
+    //replace the icon for add number with LetterTileDrawable(), so it will have identical style
+    ImageView addNumberIcon = (ImageView) getActivity().findViewById(R.id.add_number_icon);
+    LetterTileDrawable drawable = new LetterTileDrawable(getResources());
+    drawable.setLetter(ADD_BLOCKED_NUMBER_ICON_LETTER);
+    drawable.setColor(
+        ActivityCompat.getColor(getActivity(), R.color.add_blocked_number_icon_color));
+    drawable.setIsCircular(true);
+    addNumberIcon.setImageDrawable(drawable);
+
+    if (mAdapter == null) {
+      mAdapter =
+          BlockedNumbersAdapter.newBlockedNumbersAdapter(
+              getContext(), getActivity().getFragmentManager());
+    }
+    setListAdapter(mAdapter);
+
+    blockedNumbersText = (TextView) getListView().findViewById(R.id.blocked_number_text_view);
+    migratePromoView = getListView().findViewById(R.id.migrate_promo);
+    getListView().findViewById(R.id.migrate_promo_allow_button).setOnClickListener(this);
+    mImportSettings = getListView().findViewById(R.id.import_settings);
+    mBlockedNumbersDisabledForEmergency =
+        getListView().findViewById(R.id.blocked_numbers_disabled_for_emergency);
+    mBlockedNumberListDivider = getActivity().findViewById(R.id.blocked_number_list_divider);
+    getListView().findViewById(R.id.import_button).setOnClickListener(this);
+    getListView().findViewById(R.id.view_numbers_button).setOnClickListener(this);
+    getListView().findViewById(R.id.add_number_linear_layout).setOnClickListener(this);
+
+    footerText = (TextView) getActivity().findViewById(R.id.blocked_number_footer_textview);
+    mVoicemailEnabledChecker = new VisualVoicemailEnabledChecker(getContext(), this);
+    mVoicemailEnabledChecker.asyncUpdate();
+    updateActiveVoicemailProvider();
+  }
+
+  @Override
+  public void onDestroy() {
+    setListAdapter(null);
+    super.onDestroy();
+  }
+
+  @Override
+  public void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    getLoaderManager().initLoader(0, null, this);
+  }
+
+  @Override
+  public void onResume() {
+    super.onResume();
+
+    ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
+    ColorDrawable backgroundDrawable =
+        new ColorDrawable(ActivityCompat.getColor(getActivity(), R.color.dialer_theme_color));
+    actionBar.setBackgroundDrawable(backgroundDrawable);
+    actionBar.setDisplayShowCustomEnabled(false);
+    actionBar.setDisplayHomeAsUpEnabled(true);
+    actionBar.setDisplayShowHomeEnabled(true);
+    actionBar.setDisplayShowTitleEnabled(true);
+    actionBar.setTitle(R.string.manage_blocked_numbers_label);
+
+    // If the device can use the framework blocking solution, users should not be able to add
+    // new blocked numbers from the Blocked Management UI. They will be shown a promo card
+    // asking them to migrate to new blocking instead.
+    if (FilteredNumberCompat.canUseNewFiltering()) {
+      migratePromoView.setVisibility(View.VISIBLE);
+      blockedNumbersText.setVisibility(View.GONE);
+      getListView().findViewById(R.id.add_number_linear_layout).setVisibility(View.GONE);
+      getListView().findViewById(R.id.add_number_linear_layout).setOnClickListener(null);
+      mBlockedNumberListDivider.setVisibility(View.GONE);
+      mImportSettings.setVisibility(View.GONE);
+      getListView().findViewById(R.id.import_button).setOnClickListener(null);
+      getListView().findViewById(R.id.view_numbers_button).setOnClickListener(null);
+      mBlockedNumbersDisabledForEmergency.setVisibility(View.GONE);
+      footerText.setVisibility(View.GONE);
+    } else {
+      FilteredNumbersUtil.checkForSendToVoicemailContact(
+          getActivity(),
+          new CheckForSendToVoicemailContactListener() {
+            @Override
+            public void onComplete(boolean hasSendToVoicemailContact) {
+              final int visibility = hasSendToVoicemailContact ? View.VISIBLE : View.GONE;
+              mImportSettings.setVisibility(visibility);
+            }
+          });
+    }
+
+    // All views except migrate and the block list are hidden when new filtering is available
+    if (!FilteredNumberCompat.canUseNewFiltering()
+        && FilteredNumbersUtil.hasRecentEmergencyCall(getContext())) {
+      mBlockedNumbersDisabledForEmergency.setVisibility(View.VISIBLE);
+    } else {
+      mBlockedNumbersDisabledForEmergency.setVisibility(View.GONE);
+    }
+
+    mVoicemailEnabledChecker.asyncUpdate();
+  }
+
+  @Override
+  public View onCreateView(
+      LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+    return inflater.inflate(R.layout.blocked_number_fragment, container, false);
+  }
+
+  @Override
+  public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+    final String[] projection = {
+      FilteredNumberContract.FilteredNumberColumns._ID,
+      FilteredNumberContract.FilteredNumberColumns.COUNTRY_ISO,
+      FilteredNumberContract.FilteredNumberColumns.NUMBER,
+      FilteredNumberContract.FilteredNumberColumns.NORMALIZED_NUMBER
+    };
+    final String selection =
+        FilteredNumberContract.FilteredNumberColumns.TYPE
+            + "="
+            + FilteredNumberContract.FilteredNumberTypes.BLOCKED_NUMBER;
+    return new CursorLoader(
+        getContext(),
+        FilteredNumberContract.FilteredNumber.CONTENT_URI,
+        projection,
+        selection,
+        null,
+        null);
+  }
+
+  @Override
+  public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+    mAdapter.swapCursor(data);
+    if (FilteredNumberCompat.canUseNewFiltering() || data.getCount() == 0) {
+      mBlockedNumberListDivider.setVisibility(View.INVISIBLE);
+    } else {
+      mBlockedNumberListDivider.setVisibility(View.VISIBLE);
+    }
+  }
+
+  @Override
+  public void onLoaderReset(Loader<Cursor> loader) {
+    mAdapter.swapCursor(null);
+  }
+
+  @Override
+  public void onClick(final View view) {
+    final BlockedNumbersSettingsActivity activity = (BlockedNumbersSettingsActivity) getActivity();
+    if (activity == null) {
+      return;
+    }
+
+    int resId = view.getId();
+    if (resId == R.id.add_number_linear_layout) {
+      activity.showSearchUi();
+    } else if (resId == R.id.view_numbers_button) {
+      activity.showNumbersToImportPreviewUi();
+    } else if (resId == R.id.import_button) {
+      FilteredNumbersUtil.importSendToVoicemailContacts(
+          activity,
+          new ImportSendToVoicemailContactsListener() {
+            @Override
+            public void onImportComplete() {
+              mImportSettings.setVisibility(View.GONE);
+            }
+          });
+    } else if (resId == R.id.migrate_promo_allow_button) {
+      view.setEnabled(false);
+      (blockedNumbersMigratorForTest != null
+              ? blockedNumbersMigratorForTest
+              : new BlockedNumbersMigrator(getContext()))
+          .migrate(
+              new Listener() {
+                @Override
+                public void onComplete() {
+                  getContext()
+                      .startActivity(
+                          FilteredNumberCompat.createManageBlockedNumbersIntent(getContext()));
+                  // Remove this activity from the backstack
+                  activity.finish();
+                }
+              });
+    }
+  }
+
+  @Override
+  public void onVisualVoicemailEnabledStatusChanged(boolean newStatus) {
+    updateActiveVoicemailProvider();
+  }
+
+  private void updateActiveVoicemailProvider() {
+    if (getActivity() == null || getActivity().isFinishing()) {
+      return;
+    }
+    if (mVoicemailEnabledChecker.isVisualVoicemailEnabled()) {
+      footerText.setText(R.string.block_number_footer_message_vvm);
+    } else {
+      footerText.setText(R.string.block_number_footer_message_no_vvm);
+    }
+  }
+
+  void setBlockedNumbersMigratorForTest(BlockedNumbersMigrator blockedNumbersMigrator) {
+    blockedNumbersMigratorForTest = blockedNumbersMigrator;
+  }
+}
diff --git a/java/com/android/dialer/app/filterednumber/BlockedNumbersSettingsActivity.java b/java/com/android/dialer/app/filterednumber/BlockedNumbersSettingsActivity.java
new file mode 100644
index 0000000..858d283
--- /dev/null
+++ b/java/com/android/dialer/app/filterednumber/BlockedNumbersSettingsActivity.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.dialer.app.filterednumber;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.MenuItem;
+import com.android.dialer.app.R;
+import com.android.dialer.app.list.BlockedListSearchFragment;
+import com.android.dialer.app.list.SearchFragment;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.ScreenEvent;
+
+/** TODO(calderwoodra): documentation */
+public class BlockedNumbersSettingsActivity extends AppCompatActivity
+    implements SearchFragment.HostInterface {
+
+  private static final String TAG_BLOCKED_MANAGEMENT_FRAGMENT = "blocked_management";
+  private static final String TAG_BLOCKED_SEARCH_FRAGMENT = "blocked_search";
+  private static final String TAG_VIEW_NUMBERS_TO_IMPORT_FRAGMENT = "view_numbers_to_import";
+
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    setContentView(R.layout.blocked_numbers_activity);
+
+    // If savedInstanceState != null, the Activity will automatically restore the last fragment.
+    if (savedInstanceState == null) {
+      showManagementUi();
+    }
+  }
+
+  /** Shows fragment with the list of currently blocked numbers and settings related to blocking. */
+  public void showManagementUi() {
+    BlockedNumbersFragment fragment =
+        (BlockedNumbersFragment)
+            getFragmentManager().findFragmentByTag(TAG_BLOCKED_MANAGEMENT_FRAGMENT);
+    if (fragment == null) {
+      fragment = new BlockedNumbersFragment();
+    }
+
+    getFragmentManager()
+        .beginTransaction()
+        .replace(R.id.blocked_numbers_activity_container, fragment, TAG_BLOCKED_MANAGEMENT_FRAGMENT)
+        .commit();
+
+    Logger.get(this).logScreenView(ScreenEvent.Type.BLOCKED_NUMBER_MANAGEMENT, this);
+  }
+
+  /** Shows fragment with search UI for browsing/finding numbers to block. */
+  public void showSearchUi() {
+    BlockedListSearchFragment fragment =
+        (BlockedListSearchFragment)
+            getFragmentManager().findFragmentByTag(TAG_BLOCKED_SEARCH_FRAGMENT);
+    if (fragment == null) {
+      fragment = new BlockedListSearchFragment();
+      fragment.setHasOptionsMenu(false);
+      fragment.setShowEmptyListForNullQuery(true);
+      fragment.setDirectorySearchEnabled(false);
+    }
+
+    getFragmentManager()
+        .beginTransaction()
+        .replace(R.id.blocked_numbers_activity_container, fragment, TAG_BLOCKED_SEARCH_FRAGMENT)
+        .addToBackStack(null)
+        .commit();
+
+    Logger.get(this).logScreenView(ScreenEvent.Type.BLOCKED_NUMBER_ADD_NUMBER, this);
+  }
+
+  /**
+   * Shows fragment with UI to preview the numbers of contacts currently marked as send-to-voicemail
+   * in Contacts. These numbers can be imported into Dialer's blocked number list.
+   */
+  public void showNumbersToImportPreviewUi() {
+    ViewNumbersToImportFragment fragment =
+        (ViewNumbersToImportFragment)
+            getFragmentManager().findFragmentByTag(TAG_VIEW_NUMBERS_TO_IMPORT_FRAGMENT);
+    if (fragment == null) {
+      fragment = new ViewNumbersToImportFragment();
+    }
+
+    getFragmentManager()
+        .beginTransaction()
+        .replace(
+            R.id.blocked_numbers_activity_container, fragment, TAG_VIEW_NUMBERS_TO_IMPORT_FRAGMENT)
+        .addToBackStack(null)
+        .commit();
+  }
+
+  @Override
+  public boolean onOptionsItemSelected(MenuItem item) {
+    if (item.getItemId() == android.R.id.home) {
+      onBackPressed();
+      return true;
+    }
+    return false;
+  }
+
+  @Override
+  public void onBackPressed() {
+    // TODO: Achieve back navigation without overriding onBackPressed.
+    if (getFragmentManager().getBackStackEntryCount() > 0) {
+      getFragmentManager().popBackStack();
+    } else {
+      super.onBackPressed();
+    }
+  }
+
+  @Override
+  public boolean isActionBarShowing() {
+    return false;
+  }
+
+  @Override
+  public boolean isDialpadShown() {
+    return false;
+  }
+
+  @Override
+  public int getDialpadHeight() {
+    return 0;
+  }
+
+  @Override
+  public int getActionBarHeight() {
+    return 0;
+  }
+}
diff --git a/java/com/android/dialer/app/filterednumber/NumbersAdapter.java b/java/com/android/dialer/app/filterednumber/NumbersAdapter.java
new file mode 100644
index 0000000..938a784
--- /dev/null
+++ b/java/com/android/dialer/app/filterednumber/NumbersAdapter.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.dialer.app.filterednumber;
+
+import android.app.FragmentManager;
+import android.content.Context;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.QuickContactBadge;
+import android.widget.SimpleCursorAdapter;
+import android.widget.TextView;
+import com.android.dialer.app.R;
+import com.android.dialer.compat.CompatUtils;
+import com.android.dialer.contactphoto.ContactPhotoManager;
+import com.android.dialer.contactphoto.ContactPhotoManager.DefaultImageRequest;
+import com.android.dialer.lettertile.LetterTileDrawable;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.phonenumbercache.ContactInfoHelper;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+import com.android.dialer.util.UriUtils;
+
+/** TODO(calderwoodra): documentation */
+public class NumbersAdapter extends SimpleCursorAdapter {
+
+  private final Context mContext;
+  private final FragmentManager mFragmentManager;
+  private final ContactInfoHelper mContactInfoHelper;
+  private final BidiFormatter mBidiFormatter = BidiFormatter.getInstance();
+  private final ContactPhotoManager mContactPhotoManager;
+
+  public NumbersAdapter(
+      Context context,
+      FragmentManager fragmentManager,
+      ContactInfoHelper contactInfoHelper,
+      ContactPhotoManager contactPhotoManager) {
+    super(context, R.layout.blocked_number_item, null, new String[] {}, new int[] {}, 0);
+    mContext = context;
+    mFragmentManager = fragmentManager;
+    mContactInfoHelper = contactInfoHelper;
+    mContactPhotoManager = contactPhotoManager;
+  }
+
+  public void updateView(View view, String number, String countryIso) {
+    final TextView callerName = (TextView) view.findViewById(R.id.caller_name);
+    final TextView callerNumber = (TextView) view.findViewById(R.id.caller_number);
+    final QuickContactBadge quickContactBadge =
+        (QuickContactBadge) view.findViewById(R.id.quick_contact_photo);
+    quickContactBadge.setOverlay(null);
+    if (CompatUtils.hasPrioritizedMimeType()) {
+      quickContactBadge.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE);
+    }
+
+    ContactInfo info = mContactInfoHelper.lookupNumber(number, countryIso);
+    if (info == null) {
+      info = new ContactInfo();
+      info.number = number;
+    }
+    final CharSequence locationOrType = getNumberTypeOrLocation(info);
+    final String displayNumber = getDisplayNumber(info);
+    final String displayNumberStr =
+        mBidiFormatter.unicodeWrap(displayNumber, TextDirectionHeuristics.LTR);
+
+    String nameForDefaultImage;
+    if (!TextUtils.isEmpty(info.name)) {
+      nameForDefaultImage = info.name;
+      callerName.setText(info.name);
+      callerNumber.setText(locationOrType + " " + displayNumberStr);
+    } else {
+      nameForDefaultImage = displayNumber;
+      callerName.setText(displayNumberStr);
+      if (!TextUtils.isEmpty(locationOrType)) {
+        callerNumber.setText(locationOrType);
+        callerNumber.setVisibility(View.VISIBLE);
+      } else {
+        callerNumber.setVisibility(View.GONE);
+      }
+    }
+    loadContactPhoto(info, nameForDefaultImage, quickContactBadge);
+  }
+
+  private void loadContactPhoto(ContactInfo info, String displayName, QuickContactBadge badge) {
+    final String lookupKey =
+        info.lookupUri == null ? null : UriUtils.getLookupKeyFromUri(info.lookupUri);
+    final int contactType =
+        mContactInfoHelper.isBusiness(info.sourceType)
+            ? LetterTileDrawable.TYPE_BUSINESS
+            : LetterTileDrawable.TYPE_DEFAULT;
+    final DefaultImageRequest request =
+        new DefaultImageRequest(displayName, lookupKey, contactType, true /* isCircular */);
+    badge.assignContactUri(info.lookupUri);
+    badge.setContentDescription(
+        mContext.getResources().getString(R.string.description_contact_details, displayName));
+    mContactPhotoManager.loadDirectoryPhoto(
+        badge, info.photoUri, false /* darkTheme */, true /* isCircular */, request);
+  }
+
+  private String getDisplayNumber(ContactInfo info) {
+    if (!TextUtils.isEmpty(info.formattedNumber)) {
+      return info.formattedNumber;
+    } else if (!TextUtils.isEmpty(info.number)) {
+      return info.number;
+    } else {
+      return "";
+    }
+  }
+
+  private CharSequence getNumberTypeOrLocation(ContactInfo info) {
+    if (!TextUtils.isEmpty(info.name)) {
+      return ContactsContract.CommonDataKinds.Phone.getTypeLabel(
+          mContext.getResources(), info.type, info.label);
+    } else {
+      return PhoneNumberHelper.getGeoDescription(mContext, info.number);
+    }
+  }
+
+  protected Context getContext() {
+    return mContext;
+  }
+
+  protected FragmentManager getFragmentManager() {
+    return mFragmentManager;
+  }
+}
diff --git a/java/com/android/dialer/app/filterednumber/ViewNumbersToImportAdapter.java b/java/com/android/dialer/app/filterednumber/ViewNumbersToImportAdapter.java
new file mode 100644
index 0000000..106c4fb
--- /dev/null
+++ b/java/com/android/dialer/app/filterednumber/ViewNumbersToImportAdapter.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.dialer.app.filterednumber;
+
+import android.app.FragmentManager;
+import android.content.Context;
+import android.database.Cursor;
+import android.view.View;
+import com.android.dialer.app.R;
+import com.android.dialer.blocking.FilteredNumbersUtil;
+import com.android.dialer.contactphoto.ContactPhotoManager;
+import com.android.dialer.location.GeoUtil;
+import com.android.dialer.phonenumbercache.ContactInfoHelper;
+
+/** TODO(calderwoodra): documentation */
+public class ViewNumbersToImportAdapter extends NumbersAdapter {
+
+  private ViewNumbersToImportAdapter(
+      Context context,
+      FragmentManager fragmentManager,
+      ContactInfoHelper contactInfoHelper,
+      ContactPhotoManager contactPhotoManager) {
+    super(context, fragmentManager, contactInfoHelper, contactPhotoManager);
+  }
+
+  public static ViewNumbersToImportAdapter newViewNumbersToImportAdapter(
+      Context context, FragmentManager fragmentManager) {
+    return new ViewNumbersToImportAdapter(
+        context,
+        fragmentManager,
+        new ContactInfoHelper(context, GeoUtil.getCurrentCountryIso(context)),
+        ContactPhotoManager.getInstance(context));
+  }
+
+  @Override
+  public void bindView(View view, Context context, Cursor cursor) {
+    super.bindView(view, context, cursor);
+
+    final String number = cursor.getString(FilteredNumbersUtil.PhoneQuery.NUMBER_COLUMN_INDEX);
+
+    view.findViewById(R.id.delete_button).setVisibility(View.GONE);
+    updateView(view, number, null /* countryIso */);
+  }
+}
diff --git a/java/com/android/dialer/app/filterednumber/ViewNumbersToImportFragment.java b/java/com/android/dialer/app/filterednumber/ViewNumbersToImportFragment.java
new file mode 100644
index 0000000..1de7682
--- /dev/null
+++ b/java/com/android/dialer/app/filterednumber/ViewNumbersToImportFragment.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.dialer.app.filterednumber;
+
+import android.app.ListFragment;
+import android.app.LoaderManager;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.Loader;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import com.android.dialer.app.R;
+import com.android.dialer.blocking.FilteredNumbersUtil;
+import com.android.dialer.blocking.FilteredNumbersUtil.ImportSendToVoicemailContactsListener;
+
+/** TODO(calderwoodra): documentation */
+public class ViewNumbersToImportFragment extends ListFragment
+    implements LoaderManager.LoaderCallbacks<Cursor>, View.OnClickListener {
+
+  private ViewNumbersToImportAdapter mAdapter;
+
+  @Override
+  public Context getContext() {
+    return getActivity();
+  }
+
+  @Override
+  public void onActivityCreated(Bundle savedInstanceState) {
+    super.onActivityCreated(savedInstanceState);
+
+    if (mAdapter == null) {
+      mAdapter =
+          ViewNumbersToImportAdapter.newViewNumbersToImportAdapter(
+              getContext(), getActivity().getFragmentManager());
+    }
+    setListAdapter(mAdapter);
+  }
+
+  @Override
+  public void onDestroy() {
+    setListAdapter(null);
+    super.onDestroy();
+  }
+
+  @Override
+  public void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    getLoaderManager().initLoader(0, null, this);
+  }
+
+  @Override
+  public void onResume() {
+    super.onResume();
+
+    ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
+    actionBar.setTitle(R.string.import_send_to_voicemail_numbers_label);
+    actionBar.setDisplayShowCustomEnabled(false);
+    actionBar.setDisplayHomeAsUpEnabled(true);
+    actionBar.setDisplayShowHomeEnabled(true);
+    actionBar.setDisplayShowTitleEnabled(true);
+
+    getActivity().findViewById(R.id.cancel_button).setOnClickListener(this);
+    getActivity().findViewById(R.id.import_button).setOnClickListener(this);
+  }
+
+  @Override
+  public View onCreateView(
+      LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+    return inflater.inflate(R.layout.view_numbers_to_import_fragment, container, false);
+  }
+
+  @Override
+  public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+    final CursorLoader cursorLoader =
+        new CursorLoader(
+            getContext(),
+            Phone.CONTENT_URI,
+            FilteredNumbersUtil.PhoneQuery.PROJECTION,
+            FilteredNumbersUtil.PhoneQuery.SELECT_SEND_TO_VOICEMAIL_TRUE,
+            null,
+            null);
+    return cursorLoader;
+  }
+
+  @Override
+  public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+    mAdapter.swapCursor(data);
+  }
+
+  @Override
+  public void onLoaderReset(Loader<Cursor> loader) {
+    mAdapter.swapCursor(null);
+  }
+
+  @Override
+  public void onClick(final View view) {
+    if (view.getId() == R.id.import_button) {
+      FilteredNumbersUtil.importSendToVoicemailContacts(
+          getContext(),
+          new ImportSendToVoicemailContactsListener() {
+            @Override
+            public void onImportComplete() {
+              if (getActivity() != null) {
+                getActivity().onBackPressed();
+              }
+            }
+          });
+    } else if (view.getId() == R.id.cancel_button) {
+      getActivity().onBackPressed();
+    }
+  }
+}
diff --git a/java/com/android/dialer/app/list/BlockedListSearchFragment.java b/java/com/android/dialer/app/list/BlockedListSearchFragment.java
new file mode 100644
index 0000000..bef5af7
--- /dev/null
+++ b/java/com/android/dialer/app/list/BlockedListSearchFragment.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.dialer.app.list;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.telephony.PhoneNumberUtils;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.TypedValue;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.EditText;
+import android.widget.Toast;
+import com.android.contacts.common.list.ContactEntryListAdapter;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.dialer.app.R;
+import com.android.dialer.app.widget.SearchEditTextLayout;
+import com.android.dialer.blocking.BlockNumberDialogFragment;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler.OnCheckBlockedListener;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.location.GeoUtil;
+import com.android.dialer.logging.InteractionEvent;
+import com.android.dialer.logging.Logger;
+
+/** TODO(calderwoodra): documentation */
+public class BlockedListSearchFragment extends RegularSearchFragment
+    implements BlockNumberDialogFragment.Callback {
+
+  private final TextWatcher mPhoneSearchQueryTextListener =
+      new TextWatcher() {
+        @Override
+        public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+        @Override
+        public void onTextChanged(CharSequence s, int start, int before, int count) {
+          setQueryString(s.toString());
+        }
+
+        @Override
+        public void afterTextChanged(Editable s) {}
+      };
+  private final SearchEditTextLayout.Callback mSearchLayoutCallback =
+      new SearchEditTextLayout.Callback() {
+        @Override
+        public void onBackButtonClicked() {
+          getActivity().onBackPressed();
+        }
+
+        @Override
+        public void onSearchViewClicked() {}
+      };
+  private FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler;
+  private EditText mSearchView;
+
+  @Override
+  public void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+
+    setShowEmptyListForNullQuery(true);
+    /*
+     * Pass in the empty string here so ContactEntryListFragment#setQueryString interprets it as
+     * an empty search query, rather than as an uninitalized value. In the latter case, the
+     * adapter returned by #createListAdapter is used, which populates the view with contacts.
+     * Passing in the empty string forces ContactEntryListFragment to interpret it as an empty
+     * query, which results in showing an empty view
+     */
+    setQueryString(getQueryString() == null ? "" : getQueryString());
+    mFilteredNumberAsyncQueryHandler = new FilteredNumberAsyncQueryHandler(getContext());
+  }
+
+  @Override
+  public void onResume() {
+    super.onResume();
+
+    ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
+    actionBar.setCustomView(R.layout.search_edittext);
+    actionBar.setDisplayShowCustomEnabled(true);
+    actionBar.setDisplayHomeAsUpEnabled(false);
+    actionBar.setDisplayShowHomeEnabled(false);
+
+    final SearchEditTextLayout searchEditTextLayout =
+        (SearchEditTextLayout) actionBar.getCustomView().findViewById(R.id.search_view_container);
+    searchEditTextLayout.expand(false, true);
+    searchEditTextLayout.setCallback(mSearchLayoutCallback);
+    searchEditTextLayout.setBackgroundDrawable(null);
+
+    mSearchView = (EditText) searchEditTextLayout.findViewById(R.id.search_view);
+    mSearchView.addTextChangedListener(mPhoneSearchQueryTextListener);
+    mSearchView.setHint(R.string.block_number_search_hint);
+
+    searchEditTextLayout
+        .findViewById(R.id.search_box_expanded)
+        .setBackgroundColor(getContext().getResources().getColor(android.R.color.white));
+
+    if (!TextUtils.isEmpty(getQueryString())) {
+      mSearchView.setText(getQueryString());
+    }
+
+    // TODO: Don't set custom text size; use default search text size.
+    mSearchView.setTextSize(
+        TypedValue.COMPLEX_UNIT_PX,
+        getResources().getDimension(R.dimen.blocked_number_search_text_size));
+  }
+
+  @Override
+  protected ContactEntryListAdapter createListAdapter() {
+    BlockedListSearchAdapter adapter = new BlockedListSearchAdapter(getActivity());
+    adapter.setDisplayPhotos(true);
+    // Don't show SIP addresses.
+    adapter.setUseCallableUri(false);
+    // Keep in sync with the queryString set in #onCreate
+    adapter.setQueryString(getQueryString() == null ? "" : getQueryString());
+    return adapter;
+  }
+
+  @Override
+  public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+    super.onItemClick(parent, view, position, id);
+    final int adapterPosition = position - getListView().getHeaderViewsCount();
+    final BlockedListSearchAdapter adapter = (BlockedListSearchAdapter) getAdapter();
+    final int shortcutType = adapter.getShortcutTypeFromPosition(adapterPosition);
+    final Integer blockId = (Integer) view.getTag(R.id.block_id);
+    final String number;
+    switch (shortcutType) {
+      case DialerPhoneNumberListAdapter.SHORTCUT_INVALID:
+        // Handles click on a search result, either contact or nearby places result.
+        number = adapter.getPhoneNumber(adapterPosition);
+        blockContactNumber(number, blockId);
+        break;
+      case DialerPhoneNumberListAdapter.SHORTCUT_BLOCK_NUMBER:
+        // Handles click on 'Block number' shortcut to add the user query as a number.
+        number = adapter.getQueryString();
+        blockNumber(number);
+        break;
+      default:
+        LogUtil.w(
+            "BlockedListSearchFragment.onItemClick",
+            "ignoring unsupported shortcut type: " + shortcutType);
+        break;
+    }
+  }
+
+  @Override
+  protected void onItemClick(int position, long id) {
+    // Prevent SearchFragment.onItemClicked from being called.
+  }
+
+  private void blockNumber(final String number) {
+    final String countryIso = GeoUtil.getCurrentCountryIso(getContext());
+    final OnCheckBlockedListener onCheckListener =
+        new OnCheckBlockedListener() {
+          @Override
+          public void onCheckComplete(Integer id) {
+            if (id == null) {
+              BlockNumberDialogFragment.show(
+                  id,
+                  number,
+                  countryIso,
+                  PhoneNumberUtils.formatNumber(number, countryIso),
+                  R.id.blocked_numbers_activity_container,
+                  getFragmentManager(),
+                  BlockedListSearchFragment.this);
+            } else if (id == FilteredNumberAsyncQueryHandler.INVALID_ID) {
+              Toast.makeText(
+                      getContext(),
+                      ContactDisplayUtils.getTtsSpannedPhoneNumber(
+                          getResources(), R.string.invalidNumber, number),
+                      Toast.LENGTH_SHORT)
+                  .show();
+            } else {
+              Toast.makeText(
+                      getContext(),
+                      ContactDisplayUtils.getTtsSpannedPhoneNumber(
+                          getResources(), R.string.alreadyBlocked, number),
+                      Toast.LENGTH_SHORT)
+                  .show();
+            }
+          }
+        };
+    mFilteredNumberAsyncQueryHandler.isBlockedNumber(onCheckListener, number, countryIso);
+  }
+
+  @Override
+  public void onFilterNumberSuccess() {
+    Logger.get(getContext()).logInteraction(InteractionEvent.Type.BLOCK_NUMBER_MANAGEMENT_SCREEN);
+    goBack();
+  }
+
+  @Override
+  public void onUnfilterNumberSuccess() {
+    LogUtil.e(
+        "BlockedListSearchFragment.onUnfilterNumberSuccess",
+        "unblocked a number from the BlockedListSearchFragment");
+    goBack();
+  }
+
+  private void goBack() {
+    Activity activity = getActivity();
+    if (activity == null) {
+      return;
+    }
+    activity.onBackPressed();
+  }
+
+  @Override
+  public void onChangeFilteredNumberUndo() {
+    getAdapter().notifyDataSetChanged();
+  }
+
+  private void blockContactNumber(final String number, final Integer blockId) {
+    if (blockId != null) {
+      Toast.makeText(
+              getContext(),
+              ContactDisplayUtils.getTtsSpannedPhoneNumber(
+                  getResources(), R.string.alreadyBlocked, number),
+              Toast.LENGTH_SHORT)
+          .show();
+      return;
+    }
+
+    BlockNumberDialogFragment.show(
+        blockId,
+        number,
+        GeoUtil.getCurrentCountryIso(getContext()),
+        number,
+        R.id.blocked_numbers_activity_container,
+        getFragmentManager(),
+        this);
+  }
+}
diff --git a/java/com/android/dialer/app/list/ListsFragment.java b/java/com/android/dialer/app/list/ListsFragment.java
index dc1bd94..8dbe18c 100644
--- a/java/com/android/dialer/app/list/ListsFragment.java
+++ b/java/com/android/dialer/app/list/ListsFragment.java
@@ -16,6 +16,8 @@
 
 package com.android.dialer.app.list;
 
+import static android.support.v4.view.ViewPager.SCROLL_STATE_SETTLING;
+
 import android.app.Fragment;
 import android.content.SharedPreferences;
 import android.database.ContentObserver;
@@ -77,6 +79,13 @@
   private CallLogQueryHandler mCallLogQueryHandler;
 
   private UiAction.Type[] actionTypeList;
+  private final DialerImpression.Type[] swipeImpressionList =
+      new DialerImpression.Type[DialtactsPagerAdapter.TAB_COUNT_WITH_VOICEMAIL];
+  private final DialerImpression.Type[] clickImpressionList =
+      new DialerImpression.Type[DialtactsPagerAdapter.TAB_COUNT_WITH_VOICEMAIL];
+
+  // Only for detecting page selected by swiping or clicking.
+  private boolean onPageScrolledBeforeScrollStateSettling;
 
   private final ContentObserver mVoicemailStatusObserver =
       new ContentObserver(new Handler()) {
@@ -156,6 +165,24 @@
     actionTypeList[DialtactsPagerAdapter.TAB_INDEX_VOICEMAIL] =
         UiAction.Type.CHANGE_TAB_TO_VOICEMAIL;
 
+    swipeImpressionList[DialtactsPagerAdapter.TAB_INDEX_SPEED_DIAL] =
+        DialerImpression.Type.SWITCH_TAB_TO_FAVORITE_BY_SWIPE;
+    swipeImpressionList[DialtactsPagerAdapter.TAB_INDEX_HISTORY] =
+        DialerImpression.Type.SWITCH_TAB_TO_CALL_LOG_BY_SWIPE;
+    swipeImpressionList[DialtactsPagerAdapter.TAB_INDEX_ALL_CONTACTS] =
+        DialerImpression.Type.SWITCH_TAB_TO_CONTACTS_BY_SWIPE;
+    swipeImpressionList[DialtactsPagerAdapter.TAB_INDEX_VOICEMAIL] =
+        DialerImpression.Type.SWITCH_TAB_TO_VOICEMAIL_BY_SWIPE;
+
+    clickImpressionList[DialtactsPagerAdapter.TAB_INDEX_SPEED_DIAL] =
+        DialerImpression.Type.SWITCH_TAB_TO_FAVORITE_BY_CLICK;
+    clickImpressionList[DialtactsPagerAdapter.TAB_INDEX_HISTORY] =
+        DialerImpression.Type.SWITCH_TAB_TO_CALL_LOG_BY_CLICK;
+    clickImpressionList[DialtactsPagerAdapter.TAB_INDEX_ALL_CONTACTS] =
+        DialerImpression.Type.SWITCH_TAB_TO_CONTACTS_BY_CLICK;
+    clickImpressionList[DialtactsPagerAdapter.TAB_INDEX_VOICEMAIL] =
+        DialerImpression.Type.SWITCH_TAB_TO_VOICEMAIL_BY_CLICK;
+
     String[] tabTitles = new String[DialtactsPagerAdapter.TAB_COUNT_WITH_VOICEMAIL];
     tabTitles[DialtactsPagerAdapter.TAB_INDEX_SPEED_DIAL] =
         getResources().getString(R.string.tab_speed_dial);
@@ -240,6 +267,11 @@
 
   @Override
   public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+    // onPageScrolled(0, 0, 0) is called when app launch. And we should ignore it.
+    // It's also called when swipe right from first tab, but we don't care.
+    if (positionOffsetPixels != 0) {
+      onPageScrolledBeforeScrollStateSettling = true;
+    }
     mTabIndex = mAdapter.getRtlPosition(position);
 
     final int count = mOnPageChangeListeners.size();
@@ -250,6 +282,16 @@
 
   @Override
   public void onPageSelected(int position) {
+    // onPageScrollStateChanged(SCROLL_STATE_SETTLING) must be called before this.
+    // If onPageScrolled() is called before that, the page is selected by swiping;
+    // otherwise the page is selected by clicking.
+    if (onPageScrolledBeforeScrollStateSettling) {
+      Logger.get(getContext()).logImpression(swipeImpressionList[position]);
+      onPageScrolledBeforeScrollStateSettling = false;
+    } else {
+      Logger.get(getContext()).logImpression(clickImpressionList[position]);
+    }
+
     PerformanceReport.recordClick(actionTypeList[position]);
 
     LogUtil.i("ListsFragment.onPageSelected", "position: %d", position);
@@ -275,6 +317,10 @@
 
   @Override
   public void onPageScrollStateChanged(int state) {
+    if (state != SCROLL_STATE_SETTLING) {
+      onPageScrolledBeforeScrollStateSettling = false;
+    }
+
     final int count = mOnPageChangeListeners.size();
     for (int i = 0; i < count; i++) {
       mOnPageChangeListeners.get(i).onPageScrollStateChanged(state);
diff --git a/java/com/android/dialer/app/res/layout/call_log_list_item.xml b/java/com/android/dialer/app/res/layout/call_log_list_item.xml
index e967924..2c24819 100644
--- a/java/com/android/dialer/app/res/layout/call_log_list_item.xml
+++ b/java/com/android/dialer/app/res/layout/call_log_list_item.xml
@@ -157,15 +157,6 @@
             android:orientation="vertical">
 
             <TextView
-              android:id="@+id/voicemail_transcription_branding"
-              android:layout_width="wrap_content"
-              android:layout_height="wrap_content"
-              android:textColor="@color/call_log_voicemail_transcript_branding_color"
-              android:textSize="@dimen/call_log_voicemail_transcription_text_size"
-              android:paddingBottom="2dp"
-              android:singleLine="true"/>
-
-            <TextView
               android:id="@+id/voicemail_transcription"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
@@ -175,6 +166,15 @@
               android:singleLine="false"
               android:maxLines="10"/>
 
+            <TextView
+              android:id="@+id/voicemail_transcription_branding"
+              android:layout_width="wrap_content"
+              android:layout_height="wrap_content"
+              android:textColor="@color/call_log_voicemail_transcript_branding_color"
+              android:textSize="@dimen/call_log_voicemail_transcription_text_size"
+              android:paddingTop="2dp"
+              android:singleLine="true"/>
+
           </LinearLayout>
 
         </LinearLayout>
diff --git a/java/com/android/dialer/app/res/values/strings.xml b/java/com/android/dialer/app/res/values/strings.xml
index 50e7174..841eb9c 100644
--- a/java/com/android/dialer/app/res/values/strings.xml
+++ b/java/com/android/dialer/app/res/values/strings.xml
@@ -648,14 +648,22 @@
   <!-- Label for setting that shows more information about the Phone app [CHAR LIMIT=30] -->
   <string name="about_phone_label">About</string>
 
-  <!-- Label indicating who provided the voicemail transcription [CHAR LIMIT=40] -->
+  <!-- Label indicating who provided the voicemail transcription [CHAR LIMIT=64] -->
   <string name="voicemail_transcription_branding_text">Transcribed by Google</string>
 
-  <!-- Label indicating that a voicemail transcription is in progress [CHAR LIMIT=40] -->
-  <string name="voicemail_transcription_in_progress">Google is transcribing &#8230;</string>
+  <!-- Label indicating that a voicemail transcription is in progress [CHAR LIMIT=64] -->
+  <string name="voicemail_transcription_in_progress">Google is transcribing&#8230;</string>
 
-  <!-- Label indicating that a voicemail transcription failed [CHAR LIMIT=40] -->
-  <string name="voicemail_transcription_failed">Transcript not available</string>
+  <!-- Label indicating that a voicemail transcription failed [CHAR LIMIT=64] -->
+  <string name="voicemail_transcription_failed">Transcript not available.</string>
+
+  <!-- Label indicating that a voicemail transcription failed because it was in an
+       unsupported language [CHAR LIMIT=64] -->
+  <string name="voicemail_transcription_failed_language_not_supported">Transcript not available. Language not supported.</string>
+
+  <!-- Label indicating that a voicemail transcription failed because no speech was detected
+       [CHAR LIMIT=64] -->
+  <string name="voicemail_transcription_failed_no_speech">Transcript not available. No speech detected.</string>
 
   <!-- Button text to prompt a user to open an sms conversation [CHAR LIMIT=NONE] -->
   <string name="view_conversation">View</string>
diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailTosMessageCreator.java b/java/com/android/dialer/app/voicemail/error/VoicemailTosMessageCreator.java
index baf7b99..53f4680 100644
--- a/java/com/android/dialer/app/voicemail/error/VoicemailTosMessageCreator.java
+++ b/java/com/android/dialer/app/voicemail/error/VoicemailTosMessageCreator.java
@@ -143,7 +143,7 @@
     if (isVvm3() && Vvm3VoicemailMessageCreator.PIN_NOT_SET == status.configurationState) {
       LogUtil.i(
           "VoicemailTosMessageCreator.showDeclineTosDialog", "PIN_NOT_SET, showing set PIN dialog");
-      showSetPinBeforeDeclineDialog();
+      showSetPinBeforeDeclineDialog(handle);
       return;
     }
     LogUtil.i(
@@ -180,7 +180,7 @@
     builder.show();
   }
 
-  private void showSetPinBeforeDeclineDialog() {
+  private void showSetPinBeforeDeclineDialog(PhoneAccountHandle phoneAccountHandle) {
     AlertDialog.Builder builder = new AlertDialog.Builder(context);
     builder.setMessage(R.string.verizon_terms_and_conditions_decline_set_pin_dialog_message);
     builder.setPositiveButton(
@@ -191,6 +191,7 @@
             Logger.get(context)
                 .logImpression(DialerImpression.Type.VOICEMAIL_VVM3_TOS_DECLINE_CHANGE_PIN_SHOWN);
             Intent intent = new Intent(TelephonyManager.ACTION_CONFIGURE_VOICEMAIL);
+            intent.putExtra(TelephonyManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle);
             context.startActivity(intent);
           }
         });
diff --git a/java/com/android/dialer/app/widget/SearchEditTextLayout.java b/java/com/android/dialer/app/widget/SearchEditTextLayout.java
index 2051b65..95bd12a 100644
--- a/java/com/android/dialer/app/widget/SearchEditTextLayout.java
+++ b/java/com/android/dialer/app/widget/SearchEditTextLayout.java
@@ -23,6 +23,7 @@
 import android.text.TextUtils;
 import android.text.TextWatcher;
 import android.util.AttributeSet;
+import android.view.KeyEvent;
 import android.view.View;
 import android.widget.EditText;
 import android.widget.FrameLayout;
@@ -37,6 +38,7 @@
   /* Subclass-visible for testing */
   protected boolean mIsExpanded = false;
   protected boolean mIsFadedOut = false;
+  private OnKeyListener mPreImeKeyListener;
   private int mTopMargin;
   private int mBottomMargin;
   private int mLeftMargin;
@@ -54,10 +56,20 @@
 
   private ValueAnimator mAnimator;
 
+  private Callback mCallback;
+
   public SearchEditTextLayout(Context context, AttributeSet attrs) {
     super(context, attrs);
   }
 
+  public void setPreImeKeyListener(OnKeyListener listener) {
+    mPreImeKeyListener = listener;
+  }
+
+  public void setCallback(Callback listener) {
+    mCallback = listener;
+  }
+
   @Override
   protected void onFinishInflate() {
     MarginLayoutParams params = (MarginLayoutParams) getLayoutParams();
@@ -70,7 +82,7 @@
 
     mCollapsed = findViewById(R.id.search_box_collapsed);
     mExpanded = findViewById(R.id.search_box_expanded);
-    mSearchView = mExpanded.findViewById(R.id.search_view);
+    mSearchView = (EditText) mExpanded.findViewById(R.id.search_view);
 
     mSearchIcon = findViewById(R.id.search_magnifying_glass);
     mCollapsedSearchBox = findViewById(R.id.search_box_start_search);
@@ -111,6 +123,16 @@
           }
         });
 
+    mSearchView.setOnClickListener(
+        new View.OnClickListener() {
+          @Override
+          public void onClick(View v) {
+            if (mCallback != null) {
+              mCallback.onSearchViewClicked();
+            }
+          }
+        });
+
     mSearchView.addTextChangedListener(
         new TextWatcher() {
           @Override
@@ -125,10 +147,43 @@
           public void afterTextChanged(Editable s) {}
         });
 
-    mClearButtonView.setOnClickListener(v -> mSearchView.setText(null));
+    findViewById(R.id.search_close_button)
+        .setOnClickListener(
+            new OnClickListener() {
+              @Override
+              public void onClick(View v) {
+                mSearchView.setText(null);
+              }
+            });
+
+    findViewById(R.id.search_back_button)
+        .setOnClickListener(
+            new OnClickListener() {
+              @Override
+              public void onClick(View v) {
+                if (mCallback != null) {
+                  mCallback.onBackButtonClicked();
+                }
+              }
+            });
+
     super.onFinishInflate();
   }
 
+  @Override
+  public boolean dispatchKeyEventPreIme(KeyEvent event) {
+    if (mPreImeKeyListener != null) {
+      if (mPreImeKeyListener.onKey(this, event.getKeyCode(), event)) {
+        return true;
+      }
+    }
+    return super.dispatchKeyEventPreIme(event);
+  }
+
+  public void fadeOut() {
+    fadeOut(null);
+  }
+
   public void fadeOut(AnimUtils.AnimationCallback callback) {
     AnimUtils.fadeOut(this, ANIMATION_DURATION, callback);
     mIsFadedOut = true;
@@ -269,4 +324,12 @@
     params.rightMargin = (int) (mRightMargin * fraction);
     requestLayout();
   }
+
+  /** Listener for the back button next to the search view being pressed */
+  public interface Callback {
+
+    void onBackButtonClicked();
+
+    void onSearchViewClicked();
+  }
 }
diff --git a/java/com/android/dialer/compat/telephony/TelephonyManagerCompat.java b/java/com/android/dialer/compat/telephony/TelephonyManagerCompat.java
index 22ec70c..db1dd4a 100644
--- a/java/com/android/dialer/compat/telephony/TelephonyManagerCompat.java
+++ b/java/com/android/dialer/compat/telephony/TelephonyManagerCompat.java
@@ -30,6 +30,7 @@
 import com.android.dialer.telecom.TelecomUtil;
 import java.lang.reflect.InvocationTargetException;
 
+/** Hidden APIs in {@link android.telephony.TelephonyManager}. */
 public class TelephonyManagerCompat {
 
   // TODO(maxwelb): Use public API for these constants when available
diff --git a/java/com/android/dialer/constants/ScheduledJobIds.java b/java/com/android/dialer/constants/ScheduledJobIds.java
index cf93a46..3fcbb0c 100644
--- a/java/com/android/dialer/constants/ScheduledJobIds.java
+++ b/java/com/android/dialer/constants/ScheduledJobIds.java
@@ -34,6 +34,7 @@
   public static final int VVM_DEVICE_PROVISIONED_JOB = 202;
   public static final int VVM_TRANSCRIPTION_JOB = 203;
   public static final int VVM_TRANSCRIPTION_BACKFILL_JOB = 204;
+  public static final int VVM_NOTIFICATION_JOB = 205;
 
   public static final int VOIP_REGISTRATION = 300;
 
diff --git a/java/com/android/dialer/contactsfragment/ContactsAdapter.java b/java/com/android/dialer/contactsfragment/ContactsAdapter.java
index 1389531..481574e 100644
--- a/java/com/android/dialer/contactsfragment/ContactsAdapter.java
+++ b/java/com/android/dialer/contactsfragment/ContactsAdapter.java
@@ -27,6 +27,7 @@
 import android.view.View;
 import android.view.ViewGroup;
 import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
 import com.android.dialer.contactphoto.ContactPhotoManager;
 import com.android.dialer.contactsfragment.ContactsFragment.ClickAction;
 import com.android.dialer.contactsfragment.ContactsFragment.Header;
@@ -66,6 +67,17 @@
     this.clickAction = clickAction;
     headers = cursor.getExtras().getStringArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES);
     counts = cursor.getExtras().getIntArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS);
+    if (counts != null) {
+      int sum = 0;
+      for (int count : counts) {
+        sum += count;
+      }
+
+      if (sum != cursor.getCount()) {
+        LogUtil.e(
+            "ContactsAdapter", "Count sum (%d) != cursor count (%d).", sum, cursor.getCount());
+      }
+    }
   }
 
   @Override
diff --git a/java/com/android/dialer/contactsfragment/ContactsFragment.java b/java/com/android/dialer/contactsfragment/ContactsFragment.java
index ddf00b3..7d20976 100644
--- a/java/com/android/dialer/contactsfragment/ContactsFragment.java
+++ b/java/com/android/dialer/contactsfragment/ContactsFragment.java
@@ -179,7 +179,7 @@
 
   @Override
   public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
-    if (cursor.getCount() == 0) {
+    if (cursor == null || cursor.getCount() == 0) {
       emptyContentView.setDescription(R.string.all_contacts_empty);
       emptyContentView.setActionLabel(R.string.all_contacts_empty_add_contact_action);
       emptyContentView.setVisibility(View.VISIBLE);
diff --git a/java/com/android/dialer/enrichedcall/videoshare/VideoShareListener.java b/java/com/android/dialer/enrichedcall/videoshare/VideoShareListener.java
index 0e5f79f..a7d656a 100644
--- a/java/com/android/dialer/enrichedcall/videoshare/VideoShareListener.java
+++ b/java/com/android/dialer/enrichedcall/videoshare/VideoShareListener.java
@@ -16,7 +16,9 @@
 
 package com.android.dialer.enrichedcall.videoshare;
 
+import android.content.Context;
 import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
 
 /** Receives updates when video share status has changed. */
 public interface VideoShareListener {
@@ -26,5 +28,5 @@
    * invite received or canceled, or when a session changes).
    */
   @MainThread
-  void onVideoShareChanged();
+  void onVideoShareChanged(@NonNull Context context);
 }
diff --git a/java/com/android/dialer/lightbringer/Lightbringer.java b/java/com/android/dialer/lightbringer/Lightbringer.java
index 9120b24..fa57b01 100644
--- a/java/com/android/dialer/lightbringer/Lightbringer.java
+++ b/java/com/android/dialer/lightbringer/Lightbringer.java
@@ -40,7 +40,7 @@
   Intent getIntent(@NonNull Context context, @NonNull String number);
 
   @MainThread
-  void requestUpgrade(Call call);
+  void requestUpgrade(@NonNull Context context, Call call);
 
   @MainThread
   void registerListener(@NonNull LightbringerListener listener);
diff --git a/java/com/android/dialer/lightbringer/stub/LightbringerStub.java b/java/com/android/dialer/lightbringer/stub/LightbringerStub.java
index c98ae09..a030922 100644
--- a/java/com/android/dialer/lightbringer/stub/LightbringerStub.java
+++ b/java/com/android/dialer/lightbringer/stub/LightbringerStub.java
@@ -67,7 +67,7 @@
 
   @MainThread
   @Override
-  public void requestUpgrade(Call call) {
+  public void requestUpgrade(@NonNull Context context, Call call) {
     Assert.isMainThread();
     Assert.isNotNull(call);
   }
diff --git a/java/com/android/dialer/logging/dialer_impression.proto b/java/com/android/dialer/logging/dialer_impression.proto
index ef249c2..f273a36 100644
--- a/java/com/android/dialer/logging/dialer_impression.proto
+++ b/java/com/android/dialer/logging/dialer_impression.proto
@@ -530,5 +530,22 @@
     IN_CALL_DIALPAD_NUMBER_BUTTON_PRESSED = 1265;
     IN_CALL_DIALPAD_HANG_UP_BUTTON_PRESSED = 1266;
     IN_CALL_DIALPAD_CLOSE_BUTTON_PRESSED = 1267;
+
+    // More voicemail transcription impressions
+    VVM_NOTIFICATION_CREATED_WITH_IN_PROGRESS = 1268;
+    VVM_NOTIFICATION_CREATED_WITH_TRANSCRIPTION_FAILURE = 1269;
+    VVM_NOTIFICATION_CREATED_WITH_NO_TRANSCRIPTION = 1270;
+    VVM_TRANSCRIPTION_JOB_STOPPED = 1271;
+    VVM_TRANSCRIPTION_TASK_CANCELLED = 1272;
+
+    // Swipe/click to switch tabs
+    SWITCH_TAB_TO_FAVORITE_BY_SWIPE = 1273;
+    SWITCH_TAB_TO_CALL_LOG_BY_SWIPE = 1274;
+    SWITCH_TAB_TO_CONTACTS_BY_SWIPE = 1275;
+    SWITCH_TAB_TO_VOICEMAIL_BY_SWIPE = 1276;
+    SWITCH_TAB_TO_FAVORITE_BY_CLICK = 1277;
+    SWITCH_TAB_TO_CALL_LOG_BY_CLICK = 1278;
+    SWITCH_TAB_TO_CONTACTS_BY_CLICK = 1279;
+    SWITCH_TAB_TO_VOICEMAIL_BY_CLICK = 1280;
   }
 }
diff --git a/java/com/android/dialer/proguard/proguard_release.flags b/java/com/android/dialer/proguard/proguard_release.flags
index c6bdd49..1429740 100644
--- a/java/com/android/dialer/proguard/proguard_release.flags
+++ b/java/com/android/dialer/proguard/proguard_release.flags
@@ -22,3 +22,9 @@
   static *** v(...);
   static *** isLoggable(...);
 }
+
+# This allows proguard to strip Trace code from release builds.
+-assumenosideeffects class android.os.Trace {
+  static *** beginSection(...);
+  static *** endSection(...);
+}
diff --git a/java/com/android/dialer/searchfragment/list/NewSearchFragment.java b/java/com/android/dialer/searchfragment/list/NewSearchFragment.java
index 0623d39..2527b87 100644
--- a/java/com/android/dialer/searchfragment/list/NewSearchFragment.java
+++ b/java/com/android/dialer/searchfragment/list/NewSearchFragment.java
@@ -210,7 +210,7 @@
     }
     boolean slideUp = start > end;
     Interpolator interpolator = slideUp ? AnimUtils.EASE_IN : AnimUtils.EASE_OUT;
-    int startHeight = getView().getHeight();
+    int startHeight = getActivity().findViewById(android.R.id.content).getHeight();
     int endHeight = startHeight - (end - start);
     getView().setTranslationY(start);
     getView()
diff --git a/java/com/android/dialer/searchfragment/remote/RemoteContactsCursor.java b/java/com/android/dialer/searchfragment/remote/RemoteContactsCursor.java
index e6f3c26..5d80a45 100644
--- a/java/com/android/dialer/searchfragment/remote/RemoteContactsCursor.java
+++ b/java/com/android/dialer/searchfragment/remote/RemoteContactsCursor.java
@@ -118,12 +118,19 @@
     int position = getPosition();
     // proceed backwards until we reach the header row, which contains the directory ID.
     while (moveToPrevious()) {
-      int id = getInt(getColumnIndex(COLUMN_DIRECTORY_ID));
-      if (id != -1) {
-        // return the cursor to it's original position/state
-        moveToPosition(position);
-        return id;
+      int columnIndex = getColumnIndex(COLUMN_DIRECTORY_ID);
+      if (columnIndex == -1) {
+        continue;
       }
+
+      int id = getInt(columnIndex);
+      if (id == -1) {
+        continue;
+      }
+
+      // return the cursor to it's original position/state
+      moveToPosition(position);
+      return id;
     }
     throw Assert.createIllegalStateFailException("No directory id for contact at: " + position);
   }
diff --git a/java/com/android/dialer/simulator/Simulator.java b/java/com/android/dialer/simulator/Simulator.java
index f416415..f753e5f 100644
--- a/java/com/android/dialer/simulator/Simulator.java
+++ b/java/com/android/dialer/simulator/Simulator.java
@@ -22,6 +22,7 @@
 import android.view.ActionProvider;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
 
 /** Used to add menu items to the Dialer menu to test the app using simulated calls and data. */
 public interface Simulator {
@@ -42,6 +43,7 @@
       DISCONNECT,
       STATE_CHANGE,
       DTMF,
+      SESSION_MODIFY_REQUEST,
     })
     public @interface Type {}
 
@@ -53,6 +55,7 @@
     public static final int DISCONNECT = 5;
     public static final int STATE_CHANGE = 6;
     public static final int DTMF = 7;
+    public static final int SESSION_MODIFY_REQUEST = 8;
 
     @Type public final int type;
     /** Holds event specific information. For example, for DTMF this could be the keycode. */
@@ -71,5 +74,24 @@
       this.data1 = data1;
       this.data2 = data2;
     }
+
+    @Override
+    public boolean equals(Object other) {
+      if (this == other) {
+        return true;
+      }
+      if (!(other instanceof Event)) {
+        return false;
+      }
+      Event event = (Event) other;
+      return type == event.type
+          && Objects.equals(data1, event.data1)
+          && Objects.equals(data2, event.data2);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(Integer.valueOf(type), data1, data2);
+    }
   }
 }
diff --git a/java/com/android/dialer/simulator/impl/SimulatorActionProvider.java b/java/com/android/dialer/simulator/impl/SimulatorActionProvider.java
deleted file mode 100644
index f095a59..0000000
--- a/java/com/android/dialer/simulator/impl/SimulatorActionProvider.java
+++ /dev/null
@@ -1,165 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.dialer.simulator.impl;
-
-import android.content.Context;
-import android.content.Intent;
-import android.os.AsyncTask;
-import android.provider.VoicemailContract;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.view.ActionProvider;
-import android.view.MenuItem;
-import android.view.SubMenu;
-import android.view.View;
-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.DialerExecutors;
-import com.android.dialer.databasepopulator.CallLogPopulator;
-import com.android.dialer.databasepopulator.ContactsPopulator;
-import com.android.dialer.databasepopulator.VoicemailPopulator;
-import com.android.dialer.enrichedcall.simulator.EnrichedCallSimulatorActivity;
-import com.android.dialer.persistentlog.PersistentLogger;
-
-/** Implements the simulator submenu. */
-final class SimulatorActionProvider extends ActionProvider {
-  @NonNull private final Context context;
-
-  private static class ShareLogWorker implements Worker<Void, String> {
-
-    @Nullable
-    @Override
-    public String doInBackground(Void unused) {
-      return PersistentLogger.dumpLogToString();
-    }
-  }
-
-  public SimulatorActionProvider(@NonNull Context context) {
-    super(Assert.isNotNull(context));
-    this.context = context;
-  }
-
-  @Override
-  public View onCreateActionView() {
-    LogUtil.enterBlock("SimulatorActionProvider.onCreateActionView(null)");
-    return null;
-  }
-
-  @Override
-  public View onCreateActionView(MenuItem forItem) {
-    LogUtil.enterBlock("SimulatorActionProvider.onCreateActionView(MenuItem)");
-    return null;
-  }
-
-  @Override
-  public boolean hasSubMenu() {
-    LogUtil.enterBlock("SimulatorActionProvider.hasSubMenu");
-    return true;
-  }
-
-  @Override
-  public void onPrepareSubMenu(SubMenu subMenu) {
-    super.onPrepareSubMenu(subMenu);
-    LogUtil.enterBlock("SimulatorActionProvider.onPrepareSubMenu");
-    subMenu.clear();
-
-    subMenu
-        .add("Add call")
-        .setOnMenuItemClickListener(
-            (item) -> {
-              SimulatorVoiceCall.addNewIncomingCall(context);
-              return true;
-            });
-
-    subMenu
-        .add("Notifiations")
-        .setActionProvider(SimulatorNotifications.getActionProvider(context));
-    subMenu
-        .add("Populate database")
-        .setOnMenuItemClickListener(
-            (item) -> {
-              populateDatabase();
-              return true;
-            });
-    subMenu
-        .add("Clean database")
-        .setOnMenuItemClickListener(
-            (item) -> {
-              cleanDatabase();
-              return true;
-            });
-    subMenu
-        .add("Sync Voicemail")
-        .setOnMenuItemClickListener(
-            (item) -> {
-              Intent intent = new Intent(VoicemailContract.ACTION_SYNC_VOICEMAIL);
-              context.sendBroadcast(intent);
-              return true;
-            });
-
-    subMenu
-        .add("Share persistent log")
-        .setOnMenuItemClickListener(
-            (item) -> {
-              DialerExecutors.createNonUiTaskBuilder(new ShareLogWorker())
-                  .onSuccess(
-                      (String log) -> {
-                        Intent intent = new Intent(Intent.ACTION_SEND);
-                        intent.setType("text/plain");
-                        intent.putExtra(Intent.EXTRA_TEXT, log);
-                        if (intent.resolveActivity(context.getPackageManager()) != null) {
-                          context.startActivity(intent);
-                        }
-                      })
-                  .build()
-                  .executeSerial(null);
-              return true;
-            });
-    subMenu
-        .add("Enriched call simulator")
-        .setOnMenuItemClickListener(
-            (item) -> {
-              context.startActivity(EnrichedCallSimulatorActivity.newIntent(context));
-              return true;
-            });
-  }
-
-  private void populateDatabase() {
-    new AsyncTask<Void, Void, Void>() {
-      @Override
-      public Void doInBackground(Void... params) {
-        ContactsPopulator.populateContacts(context);
-        CallLogPopulator.populateCallLog(context);
-        VoicemailPopulator.populateVoicemail(context);
-        return null;
-      }
-    }.execute();
-  }
-
-  private void cleanDatabase() {
-    new AsyncTask<Void, Void, Void>() {
-      @Override
-      public Void doInBackground(Void... params) {
-        ContactsPopulator.deleteAllContacts(context);
-        CallLogPopulator.deleteAllCallLog(context);
-        VoicemailPopulator.deleteAllVoicemail(context);
-        return null;
-      }
-    }.execute();
-  }
-}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorConnection.java b/java/com/android/dialer/simulator/impl/SimulatorConnection.java
index b462b54..70c1095 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorConnection.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorConnection.java
@@ -16,8 +16,11 @@
 
 package com.android.dialer.simulator.impl;
 
+import android.content.Context;
 import android.support.annotation.NonNull;
 import android.telecom.Connection;
+import android.telecom.ConnectionRequest;
+import android.telecom.VideoProfile;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.simulator.Simulator.Event;
@@ -30,6 +33,18 @@
   private final List<Event> events = new ArrayList<>();
   private int currentState = STATE_NEW;
 
+  SimulatorConnection(@NonNull Context context, @NonNull ConnectionRequest request) {
+    Assert.isNotNull(context);
+    Assert.isNotNull(request);
+    putExtras(request.getExtras());
+    setConnectionCapabilities(
+        CAPABILITY_MUTE
+            | CAPABILITY_SUPPORT_HOLD
+            | CAPABILITY_HOLD
+            | CAPABILITY_CAN_UPGRADE_TO_VIDEO);
+    setVideoProvider(new SimulatorVideoProvider(context, this));
+  }
+
   public void addListener(@NonNull Listener listener) {
     listeners.add(Assert.isNotNull(listener));
   }
@@ -44,9 +59,9 @@
   }
 
   @Override
-  public void onAnswer() {
+  public void onAnswer(int videoState) {
     LogUtil.enterBlock("SimulatorConnection.onAnswer");
-    onEvent(new Event(Event.ANSWER));
+    onEvent(new Event(Event.ANSWER, Integer.toString(videoState), null));
   }
 
   @Override
@@ -75,9 +90,14 @@
 
   @Override
   public void onStateChanged(int newState) {
-    LogUtil.enterBlock("SimulatorConnection.onStateChanged");
-    onEvent(new Event(Event.STATE_CHANGE, stateToString(currentState), stateToString(newState)));
+    LogUtil.i(
+        "SimulatorConnection.onStateChanged",
+        "%s -> %s",
+        stateToString(currentState),
+        stateToString(newState));
+    int oldState = currentState;
     currentState = newState;
+    onEvent(new Event(Event.STATE_CHANGE, stateToString(oldState), stateToString(newState)));
   }
 
   @Override
@@ -86,13 +106,22 @@
     onEvent(new Event(Event.DTMF, Character.toString(c), null));
   }
 
-  private void onEvent(@NonNull Event event) {
+  void onEvent(@NonNull Event event) {
     events.add(Assert.isNotNull(event));
     for (Listener listener : listeners) {
       listener.onEvent(this, event);
     }
   }
 
+  void handleSessionModifyRequest(@NonNull Event event) {
+    VideoProfile fromProfile = new VideoProfile(Integer.parseInt(event.data1));
+    VideoProfile toProfile = new VideoProfile(Integer.parseInt(event.data2));
+    setVideoState(toProfile.getVideoState());
+    getVideoProvider()
+        .receiveSessionModifyResponse(
+            Connection.VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS, fromProfile, toProfile);
+  }
+
   /** Callback for when a new event arrives. */
   public interface Listener {
     void onEvent(@NonNull SimulatorConnection connection, @NonNull Event event);
diff --git a/java/com/android/dialer/simulator/impl/SimulatorConnectionService.java b/java/com/android/dialer/simulator/impl/SimulatorConnectionService.java
index 06c2591..25d4a72 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorConnectionService.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorConnectionService.java
@@ -16,10 +16,7 @@
 
 package com.android.dialer.simulator.impl;
 
-import android.content.ComponentName;
-import android.content.Context;
 import android.net.Uri;
-import android.os.Bundle;
 import android.support.annotation.NonNull;
 import android.telecom.Connection;
 import android.telecom.ConnectionRequest;
@@ -34,72 +31,15 @@
 import java.util.ArrayList;
 import java.util.List;
 
-/** Simple connection provider to create an incoming call. This is useful for emulators. */
+/** Simple connection provider to create phone calls. This is useful for emulators. */
 public class SimulatorConnectionService extends ConnectionService {
-
-  private static final String PHONE_ACCOUNT_ID = "SIMULATOR_ACCOUNT_ID";
-  private static final String EXTRA_IS_SIMULATOR_CONNECTION = "is_simulator_connection";
   private static final List<Listener> listeners = new ArrayList<>();
   private static SimulatorConnectionService instance;
 
-  private static void register(@NonNull Context context) {
-    LogUtil.enterBlock("SimulatorConnectionService.register");
-    Assert.isNotNull(context);
-    context.getSystemService(TelecomManager.class).registerPhoneAccount(buildPhoneAccount(context));
-  }
-
-  private static void unregister(@NonNull Context context) {
-    LogUtil.enterBlock("SimulatorConnectionService.unregister");
-    Assert.isNotNull(context);
-    context
-        .getSystemService(TelecomManager.class)
-        .unregisterPhoneAccount(buildPhoneAccount(context).getAccountHandle());
-  }
-
   public static SimulatorConnectionService getInstance() {
     return instance;
   }
 
-  public static void addNewOutgoingCall(
-      @NonNull Context context, @NonNull Bundle extras, @NonNull String phoneNumber) {
-    LogUtil.enterBlock("SimulatorConnectionService.addNewOutgoingCall");
-    Assert.isNotNull(context);
-    Assert.isNotNull(extras);
-    Assert.isNotNull(phoneNumber);
-
-    register(context);
-
-    Bundle bundle = new Bundle(extras);
-    bundle.putBoolean(EXTRA_IS_SIMULATOR_CONNECTION, true);
-    Bundle outgoingCallExtras = new Bundle();
-    outgoingCallExtras.putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, bundle);
-
-    // Use the system's phone account so that these look like regular SIM call.
-    TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
-    telecomManager.placeCall(
-        Uri.fromParts(PhoneAccount.SCHEME_TEL, phoneNumber, null), outgoingCallExtras);
-  }
-
-  public static void addNewIncomingCall(
-      @NonNull Context context, @NonNull Bundle extras, @NonNull String callerId) {
-    LogUtil.enterBlock("SimulatorConnectionService.addNewIncomingCall");
-    Assert.isNotNull(context);
-    Assert.isNotNull(extras);
-    Assert.isNotNull(callerId);
-
-    register(context);
-
-    Bundle bundle = new Bundle(extras);
-    bundle.putString(TelephonyManager.EXTRA_INCOMING_NUMBER, callerId);
-    bundle.putBoolean(EXTRA_IS_SIMULATOR_CONNECTION, true);
-
-    // Use the system's phone account so that these look like regular SIM call.
-    TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
-    PhoneAccountHandle systemPhoneAccount =
-        telecomManager.getDefaultOutgoingPhoneAccount(PhoneAccount.SCHEME_TEL);
-    telecomManager.addNewIncomingCall(systemPhoneAccount, bundle);
-  }
-
   public static void addListener(@NonNull Listener listener) {
     listeners.add(Assert.isNotNull(listener));
   }
@@ -108,32 +48,6 @@
     listeners.remove(Assert.isNotNull(listener));
   }
 
-  @NonNull
-  private static PhoneAccount buildPhoneAccount(Context context) {
-    PhoneAccount.Builder builder =
-        new PhoneAccount.Builder(
-            getConnectionServiceHandle(context), "Simulator connection service");
-    List<String> uriSchemes = new ArrayList<>();
-    uriSchemes.add(PhoneAccount.SCHEME_TEL);
-
-    return builder
-        .setCapabilities(
-            PhoneAccount.CAPABILITY_CALL_PROVIDER | PhoneAccount.CAPABILITY_CONNECTION_MANAGER)
-        .setShortDescription("Simulator Connection Service")
-        .setSupportedUriSchemes(uriSchemes)
-        .build();
-  }
-
-  public static PhoneAccountHandle getConnectionServiceHandle(Context context) {
-    return new PhoneAccountHandle(
-        new ComponentName(context, SimulatorConnectionService.class), PHONE_ACCOUNT_ID);
-  }
-
-  private static Uri getPhoneNumber(ConnectionRequest request) {
-    String phoneNumber = request.getExtras().getString(TelephonyManager.EXTRA_INCOMING_NUMBER);
-    return Uri.fromParts(PhoneAccount.SCHEME_TEL, phoneNumber, null);
-  }
-
   @Override
   public void onCreate() {
     super.onCreate();
@@ -151,25 +65,19 @@
   public Connection onCreateOutgoingConnection(
       PhoneAccountHandle phoneAccount, ConnectionRequest request) {
     LogUtil.enterBlock("SimulatorConnectionService.onCreateOutgoingConnection");
-    if (!isSimulatorConnectionRequest(request)) {
+    if (!SimulatorSimCallManager.isSimulatorConnectionRequest(request)) {
       LogUtil.i(
           "SimulatorConnectionService.onCreateOutgoingConnection",
           "outgoing call not from simulator, unregistering");
-      Toast.makeText(
-              this, "Unregistering Dialer simulator, making a real phone call", Toast.LENGTH_LONG)
+      Toast.makeText(this, "Unregistering simulator, making a real phone call", Toast.LENGTH_LONG)
           .show();
-      unregister(this);
+      SimulatorSimCallManager.unregister(this);
       return null;
     }
 
-    SimulatorConnection connection = new SimulatorConnection();
+    SimulatorConnection connection = new SimulatorConnection(this, request);
     connection.setDialing();
     connection.setAddress(request.getAddress(), TelecomManager.PRESENTATION_ALLOWED);
-    connection.setConnectionCapabilities(
-        Connection.CAPABILITY_MUTE
-            | Connection.CAPABILITY_SUPPORT_HOLD
-            | Connection.CAPABILITY_HOLD);
-    connection.putExtras(request.getExtras());
 
     for (Listener listener : listeners) {
       listener.onNewOutgoingConnection(connection);
@@ -181,23 +89,19 @@
   public Connection onCreateIncomingConnection(
       PhoneAccountHandle phoneAccount, ConnectionRequest request) {
     LogUtil.enterBlock("SimulatorConnectionService.onCreateIncomingConnection");
-    if (!isSimulatorConnectionRequest(request)) {
+    if (!SimulatorSimCallManager.isSimulatorConnectionRequest(request)) {
       LogUtil.i(
           "SimulatorConnectionService.onCreateIncomingConnection",
           "incoming call not from simulator, unregistering");
-      Toast.makeText(
-              this, "Unregistering Dialer simulator, got a real incoming call", Toast.LENGTH_LONG)
+      Toast.makeText(this, "Unregistering simulator, got a real incoming call", Toast.LENGTH_LONG)
           .show();
-      unregister(this);
+      SimulatorSimCallManager.unregister(this);
       return null;
     }
 
-    SimulatorConnection connection = new SimulatorConnection();
+    SimulatorConnection connection = new SimulatorConnection(this, request);
     connection.setRinging();
     connection.setAddress(getPhoneNumber(request), TelecomManager.PRESENTATION_ALLOWED);
-    connection.setConnectionCapabilities(
-        Connection.CAPABILITY_MUTE | Connection.CAPABILITY_SUPPORT_HOLD);
-    connection.putExtras(request.getExtras());
 
     for (Listener listener : listeners) {
       listener.onNewIncomingConnection(connection);
@@ -205,9 +109,9 @@
     return connection;
   }
 
-  private static boolean isSimulatorConnectionRequest(@NonNull ConnectionRequest request) {
-    return request.getExtras() != null
-        && request.getExtras().getBoolean(EXTRA_IS_SIMULATOR_CONNECTION);
+  private static Uri getPhoneNumber(ConnectionRequest request) {
+    String phoneNumber = request.getExtras().getString(TelephonyManager.EXTRA_INCOMING_NUMBER);
+    return Uri.fromParts(PhoneAccount.SCHEME_TEL, phoneNumber, null);
   }
 
   /** Callback used to notify listeners when a new connection has been added. */
diff --git a/java/com/android/dialer/simulator/impl/SimulatorImpl.java b/java/com/android/dialer/simulator/impl/SimulatorImpl.java
index 2dd180e..d6ee5ef 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorImpl.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorImpl.java
@@ -35,6 +35,6 @@
 
   @Override
   public ActionProvider getActionProvider(Context context) {
-    return new SimulatorActionProvider(context);
+    return SimulatorMainMenu.getActionProvider(context);
   }
 }
diff --git a/java/com/android/dialer/simulator/impl/SimulatorMainMenu.java b/java/com/android/dialer/simulator/impl/SimulatorMainMenu.java
new file mode 100644
index 0000000..d663d58
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/SimulatorMainMenu.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.simulator.impl;
+
+import android.content.Context;
+import android.content.Intent;
+import android.provider.VoicemailContract;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.view.ActionProvider;
+import com.android.dialer.common.concurrent.DialerExecutor.Worker;
+import com.android.dialer.common.concurrent.DialerExecutors;
+import com.android.dialer.databasepopulator.CallLogPopulator;
+import com.android.dialer.databasepopulator.ContactsPopulator;
+import com.android.dialer.databasepopulator.VoicemailPopulator;
+import com.android.dialer.enrichedcall.simulator.EnrichedCallSimulatorActivity;
+import com.android.dialer.persistentlog.PersistentLogger;
+
+/** Implements the top level simulator menu. */
+final class SimulatorMainMenu {
+
+  static ActionProvider getActionProvider(@NonNull Context context) {
+    return new SimulatorSubMenu(context)
+        .addItem("Voice call", SimulatorVoiceCall.getActionProvider(context))
+        .addItem("IMS video", SimulatorVideoCall.getActionProvider(context))
+        .addItem("Notifications", SimulatorNotifications.getActionProvider(context))
+        .addItem("Populate database", () -> populateDatabase(context))
+        .addItem("Clean database", () -> cleanDatabase(context))
+        .addItem("Sync voicemail", () -> syncVoicemail(context))
+        .addItem("Share persistent log", () -> sharePersistentLog(context))
+        .addItem(
+            "Enriched call simulator",
+            () -> context.startActivity(EnrichedCallSimulatorActivity.newIntent(context)));
+  }
+
+  private static void populateDatabase(@NonNull Context context) {
+    DialerExecutors.createNonUiTaskBuilder(new PopulateDatabaseWorker())
+        .build()
+        .executeSerial(context);
+  }
+
+  private static void cleanDatabase(@NonNull Context context) {
+    DialerExecutors.createNonUiTaskBuilder(new CleanDatabaseWorker())
+        .build()
+        .executeSerial(context);
+  }
+
+  private static void syncVoicemail(@NonNull Context context) {
+    Intent intent = new Intent(VoicemailContract.ACTION_SYNC_VOICEMAIL);
+    context.sendBroadcast(intent);
+  }
+
+  private static void sharePersistentLog(@NonNull Context context) {
+    DialerExecutors.createNonUiTaskBuilder(new ShareLogWorker())
+        .onSuccess(
+            (String log) -> {
+              Intent intent = new Intent(Intent.ACTION_SEND);
+              intent.setType("text/plain");
+              intent.putExtra(Intent.EXTRA_TEXT, log);
+              if (intent.resolveActivity(context.getPackageManager()) != null) {
+                context.startActivity(intent);
+              }
+            })
+        .build()
+        .executeSerial(null);
+  }
+
+  private SimulatorMainMenu() {}
+
+  private static class PopulateDatabaseWorker implements Worker<Context, Void> {
+    @Nullable
+    @Override
+    public Void doInBackground(Context context) {
+      ContactsPopulator.populateContacts(context);
+      CallLogPopulator.populateCallLog(context);
+      VoicemailPopulator.populateVoicemail(context);
+      return null;
+    }
+  }
+
+  private static class CleanDatabaseWorker implements Worker<Context, Void> {
+    @Nullable
+    @Override
+    public Void doInBackground(Context context) {
+      ContactsPopulator.deleteAllContacts(context);
+      CallLogPopulator.deleteAllCallLog(context);
+      VoicemailPopulator.deleteAllVoicemail(context);
+      return null;
+    }
+  }
+
+  private static class ShareLogWorker implements Worker<Void, String> {
+    @Nullable
+    @Override
+    public String doInBackground(Void unused) {
+      return PersistentLogger.dumpLogToString();
+    }
+  }
+}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorMissedCallCreator.java b/java/com/android/dialer/simulator/impl/SimulatorMissedCallCreator.java
index 22eb967..f85f466 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorMissedCallCreator.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorMissedCallCreator.java
@@ -74,7 +74,7 @@
     extras.putInt(EXTRA_CALL_COUNT, callCount - 1);
     extras.putBoolean(EXTRA_IS_MISSED_CALL_CONNECTION, true);
 
-    SimulatorConnectionService.addNewIncomingCall(context, extras, callerId);
+    SimulatorSimCallManager.addNewIncomingCall(context, callerId, false /* isVideo */, extras);
   }
 
   private static boolean isMissedCallConnection(@NonNull Connection connection) {
diff --git a/java/com/android/dialer/simulator/impl/SimulatorNotifications.java b/java/com/android/dialer/simulator/impl/SimulatorNotifications.java
index ebe8ecd..3f402d3 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorNotifications.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorNotifications.java
@@ -20,10 +20,6 @@
 import android.provider.VoicemailContract.Voicemails;
 import android.support.annotation.NonNull;
 import android.view.ActionProvider;
-import android.view.MenuItem;
-import android.view.SubMenu;
-import android.view.View;
-import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.databasepopulator.VoicemailPopulator;
 import java.util.concurrent.TimeUnit;
@@ -33,68 +29,18 @@
   private static final int NOTIFICATION_COUNT = 12;
 
   static ActionProvider getActionProvider(@NonNull Context context) {
-    return new NotificationsActionProvider(context);
-  }
-
-  private static class NotificationsActionProvider extends ActionProvider {
-    @NonNull private final Context context;
-
-    public NotificationsActionProvider(@NonNull Context context) {
-      super(Assert.isNotNull(context));
-      this.context = context;
-    }
-
-    @Override
-    public View onCreateActionView() {
-      return null;
-    }
-
-    @Override
-    public View onCreateActionView(MenuItem forItem) {
-      return null;
-    }
-
-    @Override
-    public boolean hasSubMenu() {
-      return true;
-    }
-
-    @Override
-    public void onPrepareSubMenu(@NonNull SubMenu subMenu) {
-      LogUtil.enterBlock("NotificationsActionProvider.onPrepareSubMenu");
-      Assert.isNotNull(subMenu);
-      super.onPrepareSubMenu(subMenu);
-
-      subMenu.clear();
-      subMenu
-          .add("Missed Calls")
-          .setOnMenuItemClickListener(
-              (item) -> {
-                new SimulatorMissedCallCreator(context).start(NOTIFICATION_COUNT);
-                return true;
-              });
-      subMenu
-          .add("Voicemails")
-          .setOnMenuItemClickListener(
-              (item) -> {
-                addVoicemailNotifications(context);
-                return true;
-              });
-      subMenu
-          .add("Non spam")
-          .setOnMenuItemClickListener(
-              (item) -> {
-                new SimulatorSpamCallCreator(context, false /* isSpam */).start(NOTIFICATION_COUNT);
-                return true;
-              });
-      subMenu
-          .add("Confirm spam")
-          .setOnMenuItemClickListener(
-              (item) -> {
-                new SimulatorSpamCallCreator(context, true /* isSpam */).start(NOTIFICATION_COUNT);
-                return true;
-              });
-    }
+    return new SimulatorSubMenu(context)
+        .addItem(
+            "Missed calls", () -> new SimulatorMissedCallCreator(context).start(NOTIFICATION_COUNT))
+        .addItem("Voicemails", () -> addVoicemailNotifications(context))
+        .addItem(
+            "Non spam",
+            () ->
+                new SimulatorSpamCallCreator(context, false /* isSpam */).start(NOTIFICATION_COUNT))
+        .addItem(
+            "Confirm spam",
+            () ->
+                new SimulatorSpamCallCreator(context, true /* isSpam */).start(NOTIFICATION_COUNT));
   }
 
   private static void addVoicemailNotifications(@NonNull Context context) {
diff --git a/java/com/android/dialer/simulator/impl/SimulatorPreviewCamera.java b/java/com/android/dialer/simulator/impl/SimulatorPreviewCamera.java
new file mode 100644
index 0000000..e089f75
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/SimulatorPreviewCamera.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.simulator.impl;
+
+import android.content.Context;
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraManager;
+import android.hardware.camera2.CameraMetadata;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.params.StreamConfigurationMap;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.telecom.VideoProfile.CameraCapabilities;
+import android.util.Size;
+import android.view.Surface;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import java.util.Arrays;
+
+/**
+ * Used by the video provider to draw the local camera. The in-call UI is responsible for setting
+ * the camera (front or back) and the view to draw to. The video provider then uses this class to
+ * capture frames from the given camera and draw to the given view.
+ */
+final class SimulatorPreviewCamera {
+  @NonNull private final Context context;
+  @NonNull private final String cameraId;
+  @NonNull private final Surface surface;
+  @Nullable private CameraDevice camera;
+  private boolean isStopped;
+
+  SimulatorPreviewCamera(
+      @NonNull Context context, @NonNull String cameraId, @NonNull Surface surface) {
+    this.context = Assert.isNotNull(context);
+    this.cameraId = Assert.isNotNull(cameraId);
+    this.surface = Assert.isNotNull(surface);
+  }
+
+  void startCamera() {
+    LogUtil.enterBlock("SimulatorPreviewCamera.startCamera");
+    Assert.checkState(!isStopped);
+    try {
+      context
+          .getSystemService(CameraManager.class)
+          .openCamera(cameraId, new CameraListener(), null /* handler */);
+    } catch (CameraAccessException | SecurityException e) {
+      throw Assert.createIllegalStateFailException("camera error: " + e);
+    }
+  }
+
+  void stopCamera() {
+    LogUtil.enterBlock("SimulatorPreviewCamera.stopCamera");
+    isStopped = true;
+    if (camera != null) {
+      camera.close();
+      camera = null;
+    }
+  }
+
+  @Nullable
+  static CameraCapabilities getCameraCapabilities(
+      @NonNull Context context, @Nullable String cameraId) {
+    if (cameraId == null) {
+      LogUtil.e("SimulatorPreviewCamera.getCameraCapabilities", "null camera ID");
+      return null;
+    }
+
+    CameraManager cameraManager = context.getSystemService(CameraManager.class);
+    CameraCharacteristics characteristics;
+    try {
+      characteristics = cameraManager.getCameraCharacteristics(cameraId);
+    } catch (CameraAccessException e) {
+      throw Assert.createIllegalStateFailException("camera error: " + e);
+    }
+
+    StreamConfigurationMap map =
+        characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
+    Size previewSize = map.getOutputSizes(SurfaceTexture.class)[0];
+    LogUtil.i("SimulatorPreviewCamera.getCameraCapabilities", "preview size: " + previewSize);
+    return new CameraCapabilities(previewSize.getWidth(), previewSize.getHeight());
+  }
+
+  private final class CameraListener extends CameraDevice.StateCallback {
+    @Override
+    public void onOpened(CameraDevice camera) {
+      LogUtil.enterBlock("SimulatorPreviewCamera.CameraListener.onOpened");
+      SimulatorPreviewCamera.this.camera = camera;
+      if (isStopped) {
+        LogUtil.i("SimulatorPreviewCamera.CameraListener.onOpened", "stopped");
+        stopCamera();
+        return;
+      }
+
+      try {
+        camera.createCaptureSession(
+            Arrays.asList(Assert.isNotNull(surface)),
+            new CaptureSessionCallback(),
+            null /* handler */);
+      } catch (CameraAccessException e) {
+        throw Assert.createIllegalStateFailException("camera error: " + e);
+      }
+    }
+
+    @Override
+    public void onError(CameraDevice camera, int error) {
+      LogUtil.i("SimulatorPreviewCamera.CameraListener.onError", "error: " + error);
+      stopCamera();
+    }
+
+    @Override
+    public void onDisconnected(CameraDevice camera) {
+      LogUtil.enterBlock("SimulatorPreviewCamera.CameraListener.onDisconnected");
+      stopCamera();
+    }
+
+    @Override
+    public void onClosed(CameraDevice camera) {
+      LogUtil.enterBlock("SimulatorPreviewCamera.CameraListener.onCLosed");
+    }
+  }
+
+  private final class CaptureSessionCallback extends CameraCaptureSession.StateCallback {
+    @Override
+    public void onConfigured(@NonNull CameraCaptureSession session) {
+      LogUtil.enterBlock("SimulatorPreviewCamera.CaptureSessionCallback.onConfigured");
+
+      if (isStopped) {
+        LogUtil.i("SimulatorPreviewCamera.CaptureSessionCallback.onConfigured", "stopped");
+        stopCamera();
+        return;
+      }
+      try {
+        CaptureRequest.Builder builder = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
+        builder.addTarget(surface);
+        builder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);
+        session.setRepeatingRequest(
+            builder.build(), null /* captureCallback */, null /* handler */);
+      } catch (CameraAccessException e) {
+        throw Assert.createIllegalStateFailException("camera error: " + e);
+      }
+    }
+
+    @Override
+    public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
+      LogUtil.enterBlock("SimulatorPreviewCamera.CaptureSessionCallback.onConfigureFailed");
+    }
+  }
+}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorRemoteVideo.java b/java/com/android/dialer/simulator/impl/SimulatorRemoteVideo.java
new file mode 100644
index 0000000..b14bba3
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/SimulatorRemoteVideo.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.simulator.impl;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+import android.support.annotation.WorkerThread;
+import android.view.Surface;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+
+/**
+ * Used by the video provider to draw the remote party's video. The in-call UI is responsible for
+ * setting the view to draw to. Since the simulator doesn't have a remote party we simply draw a
+ * green screen with a ball bouncing around.
+ */
+final class SimulatorRemoteVideo {
+  @NonNull private final RenderThread thread;
+  private boolean isStopped;
+
+  SimulatorRemoteVideo(@NonNull Surface surface) {
+    thread = new RenderThread(new Renderer(surface));
+  }
+
+  void startVideo() {
+    LogUtil.enterBlock("SimulatorRemoteVideo.startVideo");
+    Assert.checkState(!isStopped);
+    thread.start();
+  }
+
+  void stopVideo() {
+    LogUtil.enterBlock("SimulatorRemoteVideo.stopVideo");
+    isStopped = true;
+    thread.quitSafely();
+  }
+
+  @VisibleForTesting
+  Runnable getRenderer() {
+    return thread.getRenderer();
+  }
+
+  private static class Renderer implements Runnable {
+    private static final int FRAME_DELAY_MILLIS = 33;
+    private static final float CIRCLE_STEP = 16.0f;
+
+    @NonNull private final Surface surface;
+    private float circleX;
+    private float circleY;
+    private float radius;
+    private double angle;
+
+    Renderer(@NonNull Surface surface) {
+      this.surface = Assert.isNotNull(surface);
+    }
+
+    @Override
+    public void run() {
+      drawFrame();
+      schedule();
+    }
+
+    @WorkerThread
+    void schedule() {
+      Assert.isWorkerThread();
+      new Handler().postDelayed(this, FRAME_DELAY_MILLIS);
+    }
+
+    @WorkerThread
+    private void drawFrame() {
+      Assert.isWorkerThread();
+      Canvas canvas;
+      try {
+        canvas = surface.lockCanvas(null /* dirtyRect */);
+      } catch (IllegalArgumentException e) {
+        // This can happen when the video fragment tears down.
+        LogUtil.e("SimulatorRemoteVideo.RenderThread.drawFrame", "unable to lock canvas", e);
+        return;
+      }
+
+      LogUtil.i(
+          "SimulatorRemoteVideo.RenderThread.drawFrame",
+          "size; %d x %d",
+          canvas.getWidth(),
+          canvas.getHeight());
+      canvas.drawColor(Color.GREEN);
+      moveCircle(canvas);
+      drawCircle(canvas);
+      surface.unlockCanvasAndPost(canvas);
+    }
+
+    @WorkerThread
+    private void moveCircle(Canvas canvas) {
+      Assert.isWorkerThread();
+      int width = canvas.getWidth();
+      int height = canvas.getHeight();
+      if (circleX == 0 && circleY == 0) {
+        circleX = width / 2.0f;
+        circleY = height / 2.0f;
+        angle = Math.PI / 4.0;
+        radius = Math.min(canvas.getWidth(), canvas.getHeight()) * 0.15f;
+      } else {
+        circleX += (float) Math.cos(angle) * CIRCLE_STEP;
+        circleY += (float) Math.sin(angle) * CIRCLE_STEP;
+        // Bounce the circle off the edge.
+        if (circleX + radius >= width
+            || circleX - radius <= 0
+            || circleY + radius >= height
+            || circleY - radius <= 0) {
+          angle += Math.PI / 2.0;
+        }
+      }
+    }
+
+    @WorkerThread
+    private void drawCircle(Canvas canvas) {
+      Assert.isWorkerThread();
+      Paint paint = new Paint();
+      paint.setColor(Color.MAGENTA);
+      paint.setStyle(Paint.Style.FILL);
+      canvas.drawCircle(circleX, circleY, radius, paint);
+    }
+  }
+
+  private static class RenderThread extends HandlerThread {
+    @NonNull private final Renderer renderer;
+
+    RenderThread(@NonNull Renderer renderer) {
+      super("SimulatorRemoteVideo");
+      this.renderer = Assert.isNotNull(renderer);
+    }
+
+    @Override
+    @WorkerThread
+    protected void onLooperPrepared() {
+      Assert.isWorkerThread();
+      renderer.schedule();
+    }
+
+    @VisibleForTesting
+    Runnable getRenderer() {
+      return renderer;
+    }
+  }
+}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorSimCallManager.java b/java/com/android/dialer/simulator/impl/SimulatorSimCallManager.java
new file mode 100644
index 0000000..33eac51
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/SimulatorSimCallManager.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.simulator.impl;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.telecom.ConnectionRequest;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telephony.TelephonyManager;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * Utility to use the simulator connection service to add phone calls. To ensure that the added
+ * calls are routed through the simulator we register ourselves as a SIM call manager using
+ * CAPABILITY_CONNECTION_MANAGER. This ensures that all calls on the device must first go through
+ * our connection service.
+ *
+ * <p>For video calls this will only work if the underlying telephony phone account also supports
+ * video. To ensure that video always works we use a separate video account. The user must manually
+ * enable this account in call settings for video calls to work.
+ */
+public class SimulatorSimCallManager {
+
+  private static final String SIM_CALL_MANAGER_ACCOUNT_ID = "SIMULATOR_ACCOUNT_ID";
+  private static final String VIDEO_PROVIDER_ACCOUNT_ID = "SIMULATOR_VIDEO_ACCOUNT_ID";
+  private static final String EXTRA_IS_SIMULATOR_CONNECTION = "is_simulator_connection";
+
+  static void register(@NonNull Context context) {
+    LogUtil.enterBlock("SimulatorSimCallManager.register");
+    Assert.isNotNull(context);
+    TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
+    telecomManager.registerPhoneAccount(buildSimCallManagerAccount(context));
+    telecomManager.registerPhoneAccount(buildVideoProviderAccount(context));
+  }
+
+  static void unregister(@NonNull Context context) {
+    LogUtil.enterBlock("SimulatorSimCallManager.unregister");
+    Assert.isNotNull(context);
+    TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
+    telecomManager.unregisterPhoneAccount(getSimCallManagerHandle(context));
+    telecomManager.unregisterPhoneAccount(getVideoProviderHandle(context));
+  }
+
+  @NonNull
+  public static String addNewOutgoingCall(
+      @NonNull Context context, @NonNull String phoneNumber, boolean isVideo) {
+    return addNewOutgoingCall(context, phoneNumber, isVideo, new Bundle());
+  }
+
+  @NonNull
+  public static String addNewOutgoingCall(
+      @NonNull Context context,
+      @NonNull String phoneNumber,
+      boolean isVideo,
+      @NonNull Bundle extras) {
+    LogUtil.enterBlock("SimulatorSimCallManager.addNewOutgoingCall");
+    Assert.isNotNull(context);
+    Assert.isNotNull(extras);
+    Assert.isNotNull(phoneNumber);
+    Assert.isNotNull(extras);
+
+    register(context);
+
+    extras = new Bundle(extras);
+    extras.putBoolean(EXTRA_IS_SIMULATOR_CONNECTION, true);
+    String connectionTag = createUniqueConnectionTag();
+    extras.putBoolean(connectionTag, true);
+
+    Bundle outgoingCallExtras = new Bundle();
+    outgoingCallExtras.putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, extras);
+    outgoingCallExtras.putParcelable(
+        TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
+        isVideo ? getVideoProviderHandle(context) : getSystemPhoneAccountHandle(context));
+
+    TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
+    try {
+      telecomManager.placeCall(
+          Uri.fromParts(PhoneAccount.SCHEME_TEL, phoneNumber, null), outgoingCallExtras);
+    } catch (SecurityException e) {
+      throw Assert.createIllegalStateFailException("Unable to place call: " + e);
+    }
+    return connectionTag;
+  }
+
+  @NonNull
+  public static String addNewIncomingCall(
+      @NonNull Context context, @NonNull String callerId, boolean isVideo) {
+    return addNewIncomingCall(context, callerId, isVideo, new Bundle());
+  }
+
+  @NonNull
+  public static String addNewIncomingCall(
+      @NonNull Context context, @NonNull String callerId, boolean isVideo, @NonNull Bundle extras) {
+    LogUtil.enterBlock("SimulatorSimCallManager.addNewIncomingCall");
+    Assert.isNotNull(context);
+    Assert.isNotNull(callerId);
+    Assert.isNotNull(extras);
+
+    register(context);
+
+    extras = new Bundle(extras);
+    extras.putString(TelephonyManager.EXTRA_INCOMING_NUMBER, callerId);
+    extras.putBoolean(EXTRA_IS_SIMULATOR_CONNECTION, true);
+    String connectionTag = createUniqueConnectionTag();
+    extras.putBoolean(connectionTag, true);
+
+    TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
+    telecomManager.addNewIncomingCall(
+        isVideo ? getVideoProviderHandle(context) : getSystemPhoneAccountHandle(context), extras);
+    return connectionTag;
+  }
+
+  @NonNull
+  private static PhoneAccount buildSimCallManagerAccount(Context context) {
+    return new PhoneAccount.Builder(getSimCallManagerHandle(context), "Simulator SIM call manager")
+        .setCapabilities(PhoneAccount.CAPABILITY_CONNECTION_MANAGER)
+        .setShortDescription("Simulator SIM call manager")
+        .setSupportedUriSchemes(Arrays.asList(PhoneAccount.SCHEME_TEL))
+        .build();
+  }
+
+  @NonNull
+  private static PhoneAccount buildVideoProviderAccount(Context context) {
+    return new PhoneAccount.Builder(getVideoProviderHandle(context), "Simulator video provider")
+        .setCapabilities(
+            PhoneAccount.CAPABILITY_CALL_PROVIDER
+                | PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING
+                | PhoneAccount.CAPABILITY_VIDEO_CALLING)
+        .setShortDescription("Simulator video provider")
+        .setSupportedUriSchemes(Arrays.asList(PhoneAccount.SCHEME_TEL))
+        .build();
+  }
+
+  @NonNull
+  public static PhoneAccountHandle getSimCallManagerHandle(@NonNull Context context) {
+    return new PhoneAccountHandle(
+        new ComponentName(context, SimulatorConnectionService.class), SIM_CALL_MANAGER_ACCOUNT_ID);
+  }
+
+  @NonNull
+  static PhoneAccountHandle getVideoProviderHandle(@NonNull Context context) {
+    return new PhoneAccountHandle(
+        new ComponentName(context, SimulatorConnectionService.class), VIDEO_PROVIDER_ACCOUNT_ID);
+  }
+
+  @NonNull
+  private static PhoneAccountHandle getSystemPhoneAccountHandle(@NonNull Context context) {
+    TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
+    List<PhoneAccountHandle> handles;
+    try {
+      handles = telecomManager.getCallCapablePhoneAccounts();
+    } catch (SecurityException e) {
+      throw Assert.createIllegalStateFailException("Unable to get phone accounts: " + e);
+    }
+    for (PhoneAccountHandle handle : handles) {
+      PhoneAccount account = telecomManager.getPhoneAccount(handle);
+      if (account.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
+        return handle;
+      }
+    }
+    throw Assert.createIllegalStateFailException("no SIM phone account available");
+  }
+
+  public static boolean isSimulatorConnectionRequest(@NonNull ConnectionRequest request) {
+    return request.getExtras() != null
+        && request.getExtras().getBoolean(EXTRA_IS_SIMULATOR_CONNECTION);
+  }
+
+  @NonNull
+  private static String createUniqueConnectionTag() {
+    int callId = new Random().nextInt();
+    return String.format("simulator_phone_call_%x", Math.abs(callId));
+  }
+
+  private SimulatorSimCallManager() {}
+}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorSpamCallCreator.java b/java/com/android/dialer/simulator/impl/SimulatorSpamCallCreator.java
index ae97bc1..757658d 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorSpamCallCreator.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorSpamCallCreator.java
@@ -91,7 +91,7 @@
     // We need to clear the call log because spam notifications are only shown for new calls.
     clearCallLog(context);
 
-    SimulatorConnectionService.addNewIncomingCall(context, extras, callerId);
+    SimulatorSimCallManager.addNewIncomingCall(context, callerId, false /* isVideo */, extras);
   }
 
   private static boolean isSpamCallConnection(@NonNull Connection connection) {
diff --git a/java/com/android/dialer/simulator/impl/SimulatorSubMenu.java b/java/com/android/dialer/simulator/impl/SimulatorSubMenu.java
new file mode 100644
index 0000000..64a2e72
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/SimulatorSubMenu.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.simulator.impl;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.view.ActionProvider;
+import android.view.MenuItem;
+import android.view.SubMenu;
+import android.view.View;
+import com.android.dialer.common.Assert;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Makes it easier to create submenus in the simulator. */
+final class SimulatorSubMenu extends ActionProvider {
+  List<Item> items = new ArrayList<>();
+
+  SimulatorSubMenu(@NonNull Context context) {
+    super(Assert.isNotNull(context));
+  }
+
+  SimulatorSubMenu addItem(@NonNull String title, @NonNull Runnable clickHandler) {
+    items.add(new Item(title, clickHandler));
+    return this;
+  }
+
+  SimulatorSubMenu addItem(@NonNull String title, @NonNull ActionProvider actionProvider) {
+    items.add(new Item(title, actionProvider));
+    return this;
+  }
+
+  @Override
+  public View onCreateActionView() {
+    return null;
+  }
+
+  @Override
+  public View onCreateActionView(MenuItem forItem) {
+    return null;
+  }
+
+  @Override
+  public boolean hasSubMenu() {
+    return true;
+  }
+
+  @Override
+  public void onPrepareSubMenu(SubMenu subMenu) {
+    super.onPrepareSubMenu(subMenu);
+    subMenu.clear();
+
+    for (Item item : items) {
+      if (item.clickHandler != null) {
+        subMenu
+            .add(item.title)
+            .setOnMenuItemClickListener(
+                (i) -> {
+                  item.clickHandler.run();
+                  return true;
+                });
+      } else {
+        subMenu.add(item.title).setActionProvider(item.actionProvider);
+      }
+    }
+  }
+
+  private static final class Item {
+    @NonNull final String title;
+    @Nullable final Runnable clickHandler;
+    @Nullable final ActionProvider actionProvider;
+
+    Item(@NonNull String title, @NonNull Runnable clickHandler) {
+      this.title = Assert.isNotNull(title);
+      this.clickHandler = Assert.isNotNull(clickHandler);
+      actionProvider = null;
+    }
+
+    Item(@NonNull String title, @NonNull ActionProvider actionProvider) {
+      this.title = Assert.isNotNull(title);
+      this.clickHandler = null;
+      this.actionProvider = Assert.isNotNull(actionProvider);
+    }
+  }
+}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorVideoCall.java b/java/com/android/dialer/simulator/impl/SimulatorVideoCall.java
new file mode 100644
index 0000000..3f00ab1
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/SimulatorVideoCall.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.simulator.impl;
+
+import android.content.Context;
+import android.content.Intent;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.telecom.Connection;
+import android.telecom.DisconnectCause;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
+import android.view.ActionProvider;
+import android.widget.Toast;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.concurrent.ThreadUtil;
+import com.android.dialer.simulator.Simulator.Event;
+
+/** Entry point in the simulator to create video calls. */
+final class SimulatorVideoCall
+    implements SimulatorConnectionService.Listener, SimulatorConnection.Listener {
+  @NonNull private final Context context;
+  private final int initialVideoCapability;
+  private final int initialVideoState;
+  @Nullable private String connectionTag;
+
+  static ActionProvider getActionProvider(@NonNull Context context) {
+    return new SimulatorSubMenu(context)
+        .addItem(
+            "Incoming one way",
+            () ->
+                new SimulatorVideoCall(context, VideoProfile.STATE_RX_ENABLED).addNewIncomingCall())
+        .addItem(
+            "Incoming two way",
+            () ->
+                new SimulatorVideoCall(context, VideoProfile.STATE_BIDIRECTIONAL)
+                    .addNewIncomingCall())
+        .addItem(
+            "Outgoing one way",
+            () ->
+                new SimulatorVideoCall(context, VideoProfile.STATE_TX_ENABLED).addNewOutgoingCall())
+        .addItem(
+            "Outgoing two way",
+            () ->
+                new SimulatorVideoCall(context, VideoProfile.STATE_BIDIRECTIONAL)
+                    .addNewOutgoingCall());
+  }
+
+  private SimulatorVideoCall(@NonNull Context context, int initialVideoState) {
+    this.context = Assert.isNotNull(context);
+    this.initialVideoCapability =
+        Connection.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL
+            | Connection.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL;
+    this.initialVideoState = initialVideoState;
+    SimulatorConnectionService.addListener(this);
+  }
+
+  private void addNewIncomingCall() {
+    if (!isVideoAccountEnabled()) {
+      showVideoAccountSettings();
+      return;
+    }
+    String callerId = "+44 (0) 20 7031 3000"; // Google London office
+    connectionTag =
+        SimulatorSimCallManager.addNewIncomingCall(context, callerId, true /* isVideo */);
+  }
+
+  private void addNewOutgoingCall() {
+    if (!isVideoAccountEnabled()) {
+      showVideoAccountSettings();
+      return;
+    }
+    String phoneNumber = "+44 (0) 20 7031 3000"; // Google London office
+    connectionTag =
+        SimulatorSimCallManager.addNewOutgoingCall(context, phoneNumber, true /* isVideo */);
+  }
+
+  @Override
+  public void onNewOutgoingConnection(@NonNull SimulatorConnection connection) {
+    if (connection.getExtras().getBoolean(connectionTag)) {
+      LogUtil.i("SimulatorVideoCall.onNewOutgoingConnection", "connection created");
+      handleNewConnection(connection);
+      // Telecom will force the connection to switch to Dialing when we return it. Wait until after
+      // we're returned it before changing call state.
+      ThreadUtil.postOnUiThread(() -> connection.setActive());
+    }
+  }
+
+  @Override
+  public void onNewIncomingConnection(@NonNull SimulatorConnection connection) {
+    if (connection.getExtras().getBoolean(connectionTag)) {
+      LogUtil.i("SimulatorVideoCall.onNewIncomingConnection", "connection created");
+      handleNewConnection(connection);
+    }
+  }
+
+  private boolean isVideoAccountEnabled() {
+    SimulatorSimCallManager.register(context);
+    return context
+        .getSystemService(TelecomManager.class)
+        .getPhoneAccount(SimulatorSimCallManager.getVideoProviderHandle(context))
+        .isEnabled();
+  }
+
+  private void showVideoAccountSettings() {
+    context.startActivity(new Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS));
+    Toast.makeText(context, "Please enable simulator video provider", Toast.LENGTH_LONG).show();
+  }
+
+  private void handleNewConnection(@NonNull SimulatorConnection connection) {
+    connection.addListener(this);
+    connection.setConnectionCapabilities(
+        connection.getConnectionCapabilities() | initialVideoCapability);
+    connection.setVideoState(initialVideoState);
+  }
+
+  @Override
+  public void onEvent(@NonNull SimulatorConnection connection, @NonNull Event event) {
+    switch (event.type) {
+      case Event.NONE:
+        throw Assert.createIllegalStateFailException();
+      case Event.ANSWER:
+        connection.setVideoState(Integer.parseInt(event.data1));
+        connection.setActive();
+        break;
+      case Event.REJECT:
+        connection.setDisconnected(new DisconnectCause(DisconnectCause.REJECTED));
+        break;
+      case Event.HOLD:
+        connection.setOnHold();
+        break;
+      case Event.UNHOLD:
+        connection.setActive();
+        break;
+      case Event.DISCONNECT:
+        connection.setDisconnected(new DisconnectCause(DisconnectCause.LOCAL));
+        break;
+      case Event.STATE_CHANGE:
+        break;
+      case Event.DTMF:
+        break;
+      case Event.SESSION_MODIFY_REQUEST:
+        ThreadUtil.postDelayedOnUiThread(() -> connection.handleSessionModifyRequest(event), 2000);
+        break;
+      default:
+        throw Assert.createIllegalStateFailException();
+    }
+  }
+}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorVideoProvider.java b/java/com/android/dialer/simulator/impl/SimulatorVideoProvider.java
new file mode 100644
index 0000000..a596728
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/SimulatorVideoProvider.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.simulator.impl;
+
+import android.content.Context;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.telecom.Connection;
+import android.telecom.VideoProfile;
+import android.view.Surface;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.simulator.Simulator.Event;
+
+/**
+ * Implements the telecom video provider API to simulate IMS video calling. A video capable phone
+ * always has one video provider associated with it. Actual drawing of local and remote video is
+ * done by {@link SimulatorPreviewCamera} and {@link SimulatorRemoteVideo} respectively.
+ */
+final class SimulatorVideoProvider extends Connection.VideoProvider {
+  @NonNull private final Context context;
+  @NonNull private final SimulatorConnection connection;
+  @Nullable private String previewCameraId;;
+  @Nullable private SimulatorPreviewCamera simulatorPreviewCamera;
+  @Nullable private SimulatorRemoteVideo simulatorRemoteVideo;
+
+  SimulatorVideoProvider(@NonNull Context context, @NonNull SimulatorConnection connection) {
+    this.context = Assert.isNotNull(context);
+    this.connection = Assert.isNotNull(connection);
+  }
+
+  @Override
+  public void onSetCamera(String previewCameraId) {
+    LogUtil.i("SimulatorVideoProvider.onSetCamera", "previewCameraId: " + previewCameraId);
+    this.previewCameraId = previewCameraId;
+    if (simulatorPreviewCamera != null) {
+      simulatorPreviewCamera.stopCamera();
+      simulatorPreviewCamera = null;
+    }
+  }
+
+  @Override
+  public void onSetPreviewSurface(Surface surface) {
+    LogUtil.enterBlock("SimulatorVideoProvider.onSetPreviewSurface");
+    if (simulatorPreviewCamera != null) {
+      simulatorPreviewCamera.stopCamera();
+      simulatorPreviewCamera = null;
+    }
+    if (surface != null && previewCameraId != null) {
+      simulatorPreviewCamera = new SimulatorPreviewCamera(context, previewCameraId, surface);
+      simulatorPreviewCamera.startCamera();
+    }
+  }
+
+  @Override
+  public void onSetDisplaySurface(Surface surface) {
+    LogUtil.enterBlock("SimulatorVideoProvider.onSetDisplaySurface");
+    if (simulatorRemoteVideo != null) {
+      simulatorRemoteVideo.stopVideo();
+      simulatorRemoteVideo = null;
+    }
+    if (surface != null) {
+      simulatorRemoteVideo = new SimulatorRemoteVideo(surface);
+      simulatorRemoteVideo.startVideo();
+    }
+  }
+
+  @Override
+  public void onSetDeviceOrientation(int rotation) {
+    LogUtil.i("SimulatorVideoProvider.onSetDeviceOrientation", "rotation: " + rotation);
+  }
+
+  @Override
+  public void onSetZoom(float value) {
+    LogUtil.i("SimulatorVideoProvider.onSetZoom", "zoom: " + value);
+  }
+
+  @Override
+  public void onSendSessionModifyRequest(VideoProfile fromProfile, VideoProfile toProfile) {
+    LogUtil.enterBlock("SimulatorVideoProvider.onSendSessionModifyRequest");
+    connection.onEvent(
+        new Event(
+            Event.SESSION_MODIFY_REQUEST,
+            Integer.toString(fromProfile.getVideoState()),
+            Integer.toString(toProfile.getVideoState())));
+  }
+
+  @Override
+  public void onSendSessionModifyResponse(VideoProfile responseProfile) {
+    LogUtil.enterBlock("SimulatorVideoProvider.onSendSessionModifyResponse");
+  }
+
+  @Override
+  public void onRequestCameraCapabilities() {
+    LogUtil.enterBlock("SimulatorVideoProvider.onRequestCameraCapabilities");
+    changeCameraCapabilities(
+        SimulatorPreviewCamera.getCameraCapabilities(context, previewCameraId));
+  }
+
+  @Override
+  public void onRequestConnectionDataUsage() {
+    LogUtil.enterBlock("SimulatorVideoProvider.onRequestConnectionDataUsage");
+    setCallDataUsage(10 * 1024);
+  }
+
+  @Override
+  public void onSetPauseImage(Uri uri) {
+    LogUtil.enterBlock("SimulatorVideoProvider.onSetPauseImage");
+  }
+}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java b/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java
index 2512828..8eefb48 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java
@@ -17,18 +17,103 @@
 package com.android.dialer.simulator.impl;
 
 import android.content.Context;
-import android.os.Bundle;
 import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.telecom.Connection;
+import android.telecom.DisconnectCause;
+import android.view.ActionProvider;
+import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.concurrent.ThreadUtil;
+import com.android.dialer.simulator.Simulator.Event;
 
-/** Utilities to simulate phone calls. */
-final class SimulatorVoiceCall {
-  static void addNewIncomingCall(@NonNull Context context) {
-    LogUtil.enterBlock("SimulatorVoiceCall.addNewIncomingCall");
-    // Set the caller ID to the Google London office.
-    String callerId = "+44 (0) 20 7031 3000";
-    SimulatorConnectionService.addNewIncomingCall(context, new Bundle(), callerId);
+/** Entry point in the simulator to create voice calls. */
+final class SimulatorVoiceCall
+    implements SimulatorConnectionService.Listener, SimulatorConnection.Listener {
+  @NonNull private final Context context;
+  @Nullable private String connectionTag;
+
+  static ActionProvider getActionProvider(@NonNull Context context) {
+    return new SimulatorSubMenu(context)
+        .addItem("Incoming call", () -> new SimulatorVoiceCall(context).addNewIncomingCall(false))
+        .addItem("Outgoing call", () -> new SimulatorVoiceCall(context).addNewOutgoingCall())
+        .addItem("Spam call", () -> new SimulatorVoiceCall(context).addNewIncomingCall(true));
   }
 
-  private SimulatorVoiceCall() {}
+  private SimulatorVoiceCall(@NonNull Context context) {
+    this.context = Assert.isNotNull(context);
+    SimulatorConnectionService.addListener(this);
+  }
+
+  private void addNewIncomingCall(boolean isSpam) {
+    String callerId =
+        isSpam
+            ? "+1-661-778-3020" /* Blacklisted custom spam number */
+            : "+44 (0) 20 7031 3000" /* Google London office */;
+    connectionTag =
+        SimulatorSimCallManager.addNewIncomingCall(context, callerId, false /* isVideo */);
+  }
+
+  private void addNewOutgoingCall() {
+    String callerId = "+55-31-2128-6800"; // Brazil office.
+    connectionTag =
+        SimulatorSimCallManager.addNewOutgoingCall(context, callerId, false /* isVideo */);
+  }
+
+  @Override
+  public void onNewOutgoingConnection(@NonNull SimulatorConnection connection) {
+    if (connection.getExtras().getBoolean(connectionTag)) {
+      LogUtil.i("SimulatorVoiceCall.onNewOutgoingConnection", "connection created");
+      handleNewConnection(connection);
+      connection.setActive();
+    }
+  }
+
+  @Override
+  public void onNewIncomingConnection(@NonNull SimulatorConnection connection) {
+    if (connection.getExtras().getBoolean(connectionTag)) {
+      LogUtil.i("SimulatorVoiceCall.onNewIncomingConnection", "connection created");
+      handleNewConnection(connection);
+    }
+  }
+
+  private void handleNewConnection(@NonNull SimulatorConnection connection) {
+    connection.addListener(this);
+    connection.setConnectionCapabilities(
+        connection.getConnectionCapabilities()
+            | Connection.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL
+            | Connection.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL);
+  }
+
+  @Override
+  public void onEvent(@NonNull SimulatorConnection connection, @NonNull Event event) {
+    switch (event.type) {
+      case Event.NONE:
+        throw Assert.createIllegalStateFailException();
+      case Event.ANSWER:
+        connection.setActive();
+        break;
+      case Event.REJECT:
+        connection.setDisconnected(new DisconnectCause(DisconnectCause.REJECTED));
+        break;
+      case Event.HOLD:
+        connection.setOnHold();
+        break;
+      case Event.UNHOLD:
+        connection.setActive();
+        break;
+      case Event.DISCONNECT:
+        connection.setDisconnected(new DisconnectCause(DisconnectCause.LOCAL));
+        break;
+      case Event.STATE_CHANGE:
+        break;
+      case Event.DTMF:
+        break;
+      case Event.SESSION_MODIFY_REQUEST:
+        ThreadUtil.postDelayedOnUiThread(() -> connection.handleSessionModifyRequest(event), 2000);
+        break;
+      default:
+        throw Assert.createIllegalStateFailException();
+    }
+  }
 }
diff --git a/java/com/android/dialershared/bubble/g3doc/INTEGRATION.md b/java/com/android/dialershared/bubble/g3doc/INTEGRATION.md
deleted file mode 100644
index a13a605..0000000
--- a/java/com/android/dialershared/bubble/g3doc/INTEGRATION.md
+++ /dev/null
@@ -1,69 +0,0 @@
-# Floating Bubble Integration
-
-go/bubble-integration
-
-Author: keyboardr@
-
-Last Updated: 2017-06-06
-
-Floating bubbles provide a lightweight means of providing interactive UI while
-the user is away from the app. This document details the steps necessary to
-integrate these bubbles into your app.
-
-[TOC]
-
-![Floating bubble](images/bubble_collapsed.png){height=400}
-
-## Ensure Bubbles can be shown
-
-Add the `android.permission.SYSTEM_ALERT_WINDOW` permission to your manifest.
-Before you show the bubble, call `Bubble.canShowBubbles(Context)` to see if the
-user has granted you permission. If not, you can start an Activity from
-`Bubble.getRequestPermissionIntent(Context)` to navigate the user to the system
-settings to enable drawing over other apps. This is more than just a simple
-runtime permission; the user must explicitly allow you to draw over other apps
-via this system setting. System apps may have this allowed by default, but be
-sure to test.
-
-## Create your initial `BubbleInfo`
-
-Use `BubbleInfo.builder()` to populate a `BubbleInfo` with your color, main
-icon, main Intent (which should navigate back to your app), starting Y position,
-and a list of `Actions` to put in the drawer. Each `Action` will define its
-icon, user-displayable name (used for content description), Intent to perform
-when clicked, whether it is enabled (optional, default true), and whether it is
-checked (optional, default false).
-
-![Floating bubble expanded](images/bubble_expanded.png){height=400}
-
-## Create, show, and hide the Bubble
-
-Create the bubble using `Bubble.createBubble(Context, BubbleInfo)`. The `show()`
-method is safe to call at any time. If the Bubble is already showing, it is a
-no-op. `hide()` may also be called at any time and will collapse the drawer
-before hiding if already open. While `show()` will show immediately, `hide()`
-may need to wait for other operations or animations before the bubble is hidden.
-It is unlikely you will need to keep track of this, however. The bubble will be
-hidden at its next opportunity, and `hide()` will not block.
-
-![Floating bubble with state](images/bubble_state.png){height=400}
-
-## Update the Bubble's state
-
-Call `Bubble.setBubbleInfo(BubbleInfo)` to update all displayed state.
-`BubbleInfo`s are immutable, so to make a new one using an existing
-`BubbleInfo`, use `BubbleInfo.from(BubbleInfo)` to get a `Builder` with
-prepopulated info. If only the `Action` state has changed, it is more efficient
-to just call `Bubble.updateActions(List<Action>)`
-
-![Floating bubble with text](images/bubble_text.png){height=400}
-
-## Show text
-
-To temporarily replace the icon with a textual message, call
-`Bubble.showText(CharSequence)`. The text will be displayed for several seconds
-before transitioning back to the primary icon. The drawer will be closed if open
-and cannot be reopened while the text is displayed. Any calls to `hide()` will
-be deferred until after the text is done being displayed, so if you wish to show
-an ending message of some sort you may call `hide()` immediately after
-`showText(CharSequence)`.
diff --git a/java/com/android/dialershared/bubble/g3doc/images/bubble_collapsed.png b/java/com/android/dialershared/bubble/g3doc/images/bubble_collapsed.png
deleted file mode 100644
index 7ecc067..0000000
--- a/java/com/android/dialershared/bubble/g3doc/images/bubble_collapsed.png
+++ /dev/null
Binary files differ
diff --git a/java/com/android/dialershared/bubble/g3doc/images/bubble_expanded.png b/java/com/android/dialershared/bubble/g3doc/images/bubble_expanded.png
deleted file mode 100644
index cd477f3..0000000
--- a/java/com/android/dialershared/bubble/g3doc/images/bubble_expanded.png
+++ /dev/null
Binary files differ
diff --git a/java/com/android/dialershared/bubble/g3doc/images/bubble_state.png b/java/com/android/dialershared/bubble/g3doc/images/bubble_state.png
deleted file mode 100644
index 21ca8a8..0000000
--- a/java/com/android/dialershared/bubble/g3doc/images/bubble_state.png
+++ /dev/null
Binary files differ
diff --git a/java/com/android/dialershared/bubble/g3doc/images/bubble_text.png b/java/com/android/dialershared/bubble/g3doc/images/bubble_text.png
deleted file mode 100644
index 9c476dc..0000000
--- a/java/com/android/dialershared/bubble/g3doc/images/bubble_text.png
+++ /dev/null
Binary files differ
diff --git a/java/com/android/incallui/AnswerScreenPresenter.java b/java/com/android/incallui/AnswerScreenPresenter.java
index d530401..58231d5 100644
--- a/java/com/android/incallui/AnswerScreenPresenter.java
+++ b/java/com/android/incallui/AnswerScreenPresenter.java
@@ -104,7 +104,7 @@
                 DialerImpression.Type.VIDEO_CALL_REQUEST_ACCEPTED,
                 call.getUniqueCallId(),
                 call.getTimeAddedMs());
-        call.getVideoTech().acceptVideoRequest();
+        call.getVideoTech().acceptVideoRequest(context);
       }
     } else {
       if (answerVideoAsAudio) {
diff --git a/java/com/android/incallui/AudioRouteSelectorActivity.java b/java/com/android/incallui/AudioRouteSelectorActivity.java
index dfd4d1a..f0ae79b 100644
--- a/java/com/android/incallui/AudioRouteSelectorActivity.java
+++ b/java/com/android/incallui/AudioRouteSelectorActivity.java
@@ -32,7 +32,7 @@
   protected void onCreate(@Nullable Bundle bundle) {
     super.onCreate(bundle);
     AudioRouteSelectorDialogFragment.newInstance(AudioModeProvider.getInstance().getAudioState())
-        .show(getSupportFragmentManager(), null);
+        .show(getSupportFragmentManager(), AudioRouteSelectorDialogFragment.TAG);
   }
 
   @Override
@@ -44,4 +44,20 @@
   public void onAudioRouteSelectorDismiss() {
     finish();
   }
+
+  @Override
+  protected void onPause() {
+    super.onPause();
+    AudioRouteSelectorDialogFragment audioRouteSelectorDialogFragment =
+        (AudioRouteSelectorDialogFragment)
+            getSupportFragmentManager().findFragmentByTag(AudioRouteSelectorDialogFragment.TAG);
+    // If Android back button is pressed, the fragment is dismissed and removed. If home button is
+    // pressed, we have to manually dismiss the fragment here. The fragment is also removed when
+    // dismissed.
+    if (audioRouteSelectorDialogFragment != null) {
+      audioRouteSelectorDialogFragment.dismiss();
+    }
+    // We don't expect the activity to resume
+    finish();
+  }
 }
diff --git a/java/com/android/incallui/CallButtonPresenter.java b/java/com/android/incallui/CallButtonPresenter.java
index 658ae64..bd5bb78 100644
--- a/java/com/android/incallui/CallButtonPresenter.java
+++ b/java/com/android/incallui/CallButtonPresenter.java
@@ -294,7 +294,7 @@
             DialerImpression.Type.VIDEO_CALL_UPGRADE_REQUESTED,
             mCall.getUniqueCallId(),
             mCall.getTimeAddedMs());
-    mCall.getVideoTech().upgradeToVideo();
+    mCall.getVideoTech().upgradeToVideo(mContext);
   }
 
   @Override
@@ -360,7 +360,7 @@
     } else {
       updateCamera(
           InCallPresenter.getInstance().getInCallCameraManager().isUsingFrontFacingCamera());
-      mCall.getVideoTech().resumeTransmission();
+      mCall.getVideoTech().resumeTransmission(mContext);
     }
 
     mInCallButtonUi.setVideoPaused(pause);
diff --git a/java/com/android/incallui/NotificationBroadcastReceiver.java b/java/com/android/incallui/NotificationBroadcastReceiver.java
index 5e757cf..0daa017 100644
--- a/java/com/android/incallui/NotificationBroadcastReceiver.java
+++ b/java/com/android/incallui/NotificationBroadcastReceiver.java
@@ -95,7 +95,7 @@
     } else {
       DialerCall call = callList.getVideoUpgradeRequestCall();
       if (call != null) {
-        call.getVideoTech().acceptVideoRequest();
+        call.getVideoTech().acceptVideoRequest(context);
       }
     }
   }
diff --git a/java/com/android/incallui/ReturnToCallController.java b/java/com/android/incallui/ReturnToCallController.java
index e54102c..fd48b37 100644
--- a/java/com/android/incallui/ReturnToCallController.java
+++ b/java/com/android/incallui/ReturnToCallController.java
@@ -23,16 +23,16 @@
 import android.support.annotation.NonNull;
 import android.support.annotation.VisibleForTesting;
 import android.telecom.CallAudioState;
+import com.android.bubble.Bubble;
+import com.android.bubble.Bubble.BubbleExpansionStateListener;
+import com.android.bubble.Bubble.ExpansionState;
+import com.android.bubble.BubbleInfo;
+import com.android.bubble.BubbleInfo.Action;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.configprovider.ConfigProviderBindings;
 import com.android.dialer.logging.DialerImpression;
 import com.android.dialer.logging.Logger;
 import com.android.dialer.telecom.TelecomUtil;
-import com.android.dialershared.bubble.Bubble;
-import com.android.dialershared.bubble.Bubble.BubbleExpansionStateListener;
-import com.android.dialershared.bubble.Bubble.ExpansionState;
-import com.android.dialershared.bubble.BubbleInfo;
-import com.android.dialershared.bubble.BubbleInfo.Action;
 import com.android.incallui.InCallPresenter.InCallUiListener;
 import com.android.incallui.audiomode.AudioModeProvider;
 import com.android.incallui.audiomode.AudioModeProvider.AudioModeListener;
diff --git a/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java b/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java
index c7a9d63..860d2d2 100644
--- a/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java
+++ b/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java
@@ -37,6 +37,7 @@
 /** Shows picker for audio routes */
 public class AudioRouteSelectorDialogFragment extends BottomSheetDialogFragment {
 
+  public static final String TAG = "AudioRouteSelectorDialogFragment";
   private static final String ARG_AUDIO_STATE = "audio_state";
 
   /** Called when an audio route is picked */
diff --git a/java/com/android/incallui/videotech/VideoTech.java b/java/com/android/incallui/videotech/VideoTech.java
index 79a8c60..e3753bc 100644
--- a/java/com/android/incallui/videotech/VideoTech.java
+++ b/java/com/android/incallui/videotech/VideoTech.java
@@ -17,6 +17,7 @@
 package com.android.incallui.videotech;
 
 import android.content.Context;
+import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import com.android.dialer.logging.DialerImpression;
 import com.android.incallui.video.protocol.VideoCallScreen;
@@ -48,9 +49,9 @@
   @SessionModificationState
   int getSessionModificationState();
 
-  void upgradeToVideo();
+  void upgradeToVideo(@NonNull Context context);
 
-  void acceptVideoRequest();
+  void acceptVideoRequest(@NonNull Context context);
 
   void acceptVideoRequestAsAudio();
 
@@ -60,7 +61,7 @@
 
   void stopTransmission();
 
-  void resumeTransmission();
+  void resumeTransmission(@NonNull Context context);
 
   void pause();
 
diff --git a/java/com/android/incallui/videotech/empty/EmptyVideoTech.java b/java/com/android/incallui/videotech/empty/EmptyVideoTech.java
index 34dd1bf..76766df 100644
--- a/java/com/android/incallui/videotech/empty/EmptyVideoTech.java
+++ b/java/com/android/incallui/videotech/empty/EmptyVideoTech.java
@@ -17,6 +17,7 @@
 package com.android.incallui.videotech.empty;
 
 import android.content.Context;
+import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import com.android.dialer.common.Assert;
 import com.android.incallui.video.protocol.VideoCallScreen;
@@ -65,10 +66,10 @@
   }
 
   @Override
-  public void upgradeToVideo() {}
+  public void upgradeToVideo(@NonNull Context context) {}
 
   @Override
-  public void acceptVideoRequest() {}
+  public void acceptVideoRequest(@NonNull Context context) {}
 
   @Override
   public void acceptVideoRequestAsAudio() {}
@@ -85,7 +86,7 @@
   public void stopTransmission() {}
 
   @Override
-  public void resumeTransmission() {}
+  public void resumeTransmission(@NonNull Context context) {}
 
   @Override
   public void pause() {}
diff --git a/java/com/android/incallui/videotech/ims/ImsVideoCallCallback.java b/java/com/android/incallui/videotech/ims/ImsVideoCallCallback.java
index b839293..954dfcd 100644
--- a/java/com/android/incallui/videotech/ims/ImsVideoCallCallback.java
+++ b/java/com/android/incallui/videotech/ims/ImsVideoCallCallback.java
@@ -16,6 +16,7 @@
 
 package com.android.incallui.videotech.ims;
 
+import android.content.Context;
 import android.os.Handler;
 import android.telecom.Call;
 import android.telecom.Connection;
@@ -37,17 +38,20 @@
   private final Call call;
   private final ImsVideoTech videoTech;
   private final VideoTechListener listener;
+  private final Context context;
   private int requestedVideoState = VideoProfile.STATE_AUDIO_ONLY;
 
   ImsVideoCallCallback(
       final LoggingBindings logger,
       final Call call,
       ImsVideoTech videoTech,
-      VideoTechListener listener) {
+      VideoTechListener listener,
+      Context context) {
     this.logger = logger;
     this.call = call;
     this.videoTech = videoTech;
     this.listener = listener;
+    this.context = context;
   }
 
   @Override
@@ -66,10 +70,16 @@
           "ImsVideoTech.onSessionModifyRequestReceived", "call downgraded to %d", newVideoState);
     } else if (previousVideoState != newVideoState) {
       requestedVideoState = newVideoState;
-      videoTech.setSessionModificationState(
-          SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST);
-      listener.onVideoUpgradeRequestReceived();
-      logger.logImpression(DialerImpression.Type.IMS_VIDEO_REQUEST_RECEIVED);
+      if (!wasVideoCall) {
+        videoTech.setSessionModificationState(
+            SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST);
+        listener.onVideoUpgradeRequestReceived();
+        logger.logImpression(DialerImpression.Type.IMS_VIDEO_REQUEST_RECEIVED);
+      } else {
+        LogUtil.i(
+            "ImsVideoTech.onSessionModifyRequestReceived", "call updated to %d", newVideoState);
+        videoTech.acceptVideoRequest(context);
+      }
     }
   }
 
diff --git a/java/com/android/incallui/videotech/ims/ImsVideoTech.java b/java/com/android/incallui/videotech/ims/ImsVideoTech.java
index fec05dc..c12474d 100644
--- a/java/com/android/incallui/videotech/ims/ImsVideoTech.java
+++ b/java/com/android/incallui/videotech/ims/ImsVideoTech.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import android.os.Build;
+import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.telecom.Call;
 import android.telecom.Call.Details;
@@ -120,7 +121,7 @@
     }
 
     if (callback == null) {
-      callback = new ImsVideoCallCallback(logger, call, this, listener);
+      callback = new ImsVideoCallCallback(logger, call, this, listener, context);
       call.getVideoCall().registerCallback(callback);
     }
 
@@ -165,7 +166,7 @@
   }
 
   @Override
-  public void upgradeToVideo() {
+  public void upgradeToVideo(@NonNull Context context) {
     LogUtil.enterBlock("ImsVideoTech.upgradeToVideo");
 
     int unpausedVideoState = getUnpausedVideoState(call.getDetails().getVideoState());
@@ -177,7 +178,7 @@
   }
 
   @Override
-  public void acceptVideoRequest() {
+  public void acceptVideoRequest(@NonNull Context context) {
     int requestedVideoState = callback.getRequestedVideoState();
     Assert.checkArgument(requestedVideoState != VideoProfile.STATE_AUDIO_ONLY);
     LogUtil.i("ImsVideoTech.acceptUpgradeRequest", "videoState: " + requestedVideoState);
@@ -223,7 +224,7 @@
   }
 
   @Override
-  public void resumeTransmission() {
+  public void resumeTransmission(@NonNull Context context) {
     LogUtil.enterBlock("ImsVideoTech.resumeTransmission");
 
     transmissionStopped = false;
diff --git a/java/com/android/incallui/videotech/lightbringer/LightbringerTech.java b/java/com/android/incallui/videotech/lightbringer/LightbringerTech.java
index 4882ba8..a807759 100644
--- a/java/com/android/incallui/videotech/lightbringer/LightbringerTech.java
+++ b/java/com/android/incallui/videotech/lightbringer/LightbringerTech.java
@@ -21,7 +21,6 @@
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.telecom.Call;
-import com.android.contacts.common.compat.telecom.TelecomManagerCompat;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.configprovider.ConfigProviderBindings;
@@ -55,7 +54,7 @@
 
   @Override
   public boolean isAvailable(Context context) {
-    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
       LogUtil.v("LightbringerTech.isAvailable", "upgrade unavailable, only supported on O+");
       return false;
     }
@@ -71,11 +70,6 @@
       return false;
     }
 
-    if (!TelecomManagerCompat.supportsHandover()) {
-      LogUtil.v("LightbringerTech.isAvailable", "upgrade unavailable, telephony support missing");
-      return false;
-    }
-
     if (!lightbringer.supportsUpgrade(context, callingNumber)) {
       LogUtil.v("LightbringerTech.isAvailable", "upgrade unavailable, number does not support it");
       return false;
@@ -125,13 +119,13 @@
   }
 
   @Override
-  public void upgradeToVideo() {
+  public void upgradeToVideo(@NonNull Context context) {
     listener.onImpressionLoggingNeeded(DialerImpression.Type.LIGHTBRINGER_UPGRADE_REQUESTED);
-    lightbringer.requestUpgrade(call);
+    lightbringer.requestUpgrade(context, call);
   }
 
   @Override
-  public void acceptVideoRequest() {
+  public void acceptVideoRequest(@NonNull Context context) {
     throw Assert.createUnsupportedOperationFailException();
   }
 
@@ -156,7 +150,7 @@
   }
 
   @Override
-  public void resumeTransmission() {
+  public void resumeTransmission(@NonNull Context context) {
     throw Assert.createUnsupportedOperationFailException();
   }
 
diff --git a/java/com/android/voicemail/impl/ActivationTask.java b/java/com/android/voicemail/impl/ActivationTask.java
index d7a122c..29c91e0 100644
--- a/java/com/android/voicemail/impl/ActivationTask.java
+++ b/java/com/android/voicemail/impl/ActivationTask.java
@@ -170,7 +170,9 @@
 
     if (VvmAccountManager.isAccountActivated(getContext(), phoneAccountHandle)) {
       VvmLog.i(TAG, "Account is already activated");
-      onSuccess(getContext(), phoneAccountHandle);
+      // The activated state might come from restored data, the filter still needs to be set up.
+      helper.activateSmsFilter();
+      onSuccess(getContext(), phoneAccountHandle, helper);
       return;
     }
     helper.handleEvent(
@@ -230,7 +232,7 @@
             + message.getReturnCode());
     if (message.getProvisioningStatus().equals(OmtpConstants.SUBSCRIBER_READY)) {
       VvmLog.d(TAG, "subscriber ready, no activation required");
-      updateSource(getContext(), phoneAccountHandle, message);
+      updateSource(getContext(), phoneAccountHandle, message, helper);
     } else {
       if (helper.supportsProvisioning()) {
         VvmLog.i(TAG, "Subscriber not ready, start provisioning");
@@ -240,7 +242,7 @@
         VvmLog.i(TAG, "Subscriber new but provisioning is not supported");
         // Ignore the non-ready state and attempt to use the provided info as is.
         // This is probably caused by not completing the new user tutorial.
-        updateSource(getContext(), phoneAccountHandle, message);
+        updateSource(getContext(), phoneAccountHandle, message, helper);
       } else {
         VvmLog.i(TAG, "Subscriber not ready but provisioning is not supported");
         helper.handleEvent(status, OmtpEvents.CONFIG_SERVICE_NOT_AVAILABLE);
@@ -251,20 +253,23 @@
   }
 
   private static void updateSource(
-      Context context, PhoneAccountHandle phone, StatusMessage message) {
+      Context context,
+      PhoneAccountHandle phone,
+      StatusMessage message,
+      OmtpVvmCarrierConfigHelper config) {
 
     if (OmtpConstants.SUCCESS.equals(message.getReturnCode())) {
       // Save the IMAP credentials in preferences so they are persistent and can be retrieved.
       VvmAccountManager.addAccount(context, phone, message);
-      onSuccess(context, phone);
+      onSuccess(context, phone, config);
     } else {
       VvmLog.e(TAG, "Visual voicemail not available for subscriber.");
     }
   }
 
-  private static void onSuccess(Context context, PhoneAccountHandle phoneAccountHandle) {
-    OmtpVvmCarrierConfigHelper helper = new OmtpVvmCarrierConfigHelper(context, phoneAccountHandle);
-    helper.handleEvent(
+  private static void onSuccess(
+      Context context, PhoneAccountHandle phoneAccountHandle, OmtpVvmCarrierConfigHelper config) {
+    config.handleEvent(
         VoicemailStatus.edit(context, phoneAccountHandle),
         OmtpEvents.CONFIG_REQUEST_STATUS_SUCCESS);
     clearLegacyVoicemailNotification(context, phoneAccountHandle);
diff --git a/java/com/android/voicemail/impl/OmtpVvmCarrierConfigHelper.java b/java/com/android/voicemail/impl/OmtpVvmCarrierConfigHelper.java
index 700e1cb..90303f5 100644
--- a/java/com/android/voicemail/impl/OmtpVvmCarrierConfigHelper.java
+++ b/java/com/android/voicemail/impl/OmtpVvmCarrierConfigHelper.java
@@ -50,6 +50,8 @@
  * that may clutter CarrierConfigManager too much.
  *
  * <p>The current hidden configs are: {@link #getSslPort()} {@link #getDisabledCapabilities()}
+ *
+ * <p>TODO(twyen): refactor this to an interface.
  */
 @TargetApi(VERSION_CODES.O)
 public class OmtpVvmCarrierConfigHelper {
@@ -112,19 +114,19 @@
       return;
     }
 
-    mCarrierConfig = getCarrierConfig(telephonyManager);
-    mTelephonyConfig =
-        new TelephonyVvmConfigManager(context).getConfig(telephonyManager.getSimOperator());
-
-    mVvmType = getVvmType();
-    mProtocol = VisualVoicemailProtocolFactory.create(mContext.getResources(), mVvmType);
-
     if (ConfigOverrideFragment.isOverridden(context)) {
       mOverrideConfig = ConfigOverrideFragment.getConfig(context);
       VvmLog.w(TAG, "Config override is activated: " + mOverrideConfig);
     } else {
       mOverrideConfig = null;
     }
+
+    mCarrierConfig = getCarrierConfig(telephonyManager);
+    mTelephonyConfig =
+        new TelephonyVvmConfigManager(context).getConfig(telephonyManager.getSimOperator());
+
+    mVvmType = getVvmType();
+    mProtocol = VisualVoicemailProtocolFactory.create(mContext.getResources(), mVvmType);
   }
 
   @VisibleForTesting
@@ -187,7 +189,11 @@
   @Nullable
   public Set<String> getCarrierVvmPackageNames() {
     Assert.checkArgument(isValid());
-    Set<String> names = getCarrierVvmPackageNames(mCarrierConfig);
+    Set<String> names = getCarrierVvmPackageNames(mOverrideConfig);
+    if (names != null) {
+      return names;
+    }
+    names = getCarrierVvmPackageNames(mCarrierConfig);
     if (names != null) {
       return names;
     }
@@ -278,7 +284,12 @@
   @Nullable
   public Set<String> getDisabledCapabilities() {
     Assert.checkArgument(isValid());
-    Set<String> disabledCapabilities = getDisabledCapabilities(mCarrierConfig);
+    Set<String> disabledCapabilities;
+    disabledCapabilities = getDisabledCapabilities(mOverrideConfig);
+    if (disabledCapabilities != null) {
+      return disabledCapabilities;
+    }
+    disabledCapabilities = getDisabledCapabilities(mCarrierConfig);
     if (disabledCapabilities != null) {
       return disabledCapabilities;
     }
diff --git a/java/com/android/voicemail/impl/configui/ConfigOverrideFragment.java b/java/com/android/voicemail/impl/configui/ConfigOverrideFragment.java
index 1624ce5..caf33df 100644
--- a/java/com/android/voicemail/impl/configui/ConfigOverrideFragment.java
+++ b/java/com/android/voicemail/impl/configui/ConfigOverrideFragment.java
@@ -49,7 +49,8 @@
    * Any preference with key that starts with this prefix will be written to the dialer carrier
    * config.
    */
-  @VisibleForTesting static final String CONFIG_OVERRIDE_KEY_PREFIX = "vvm_config_override_key_";
+  @VisibleForTesting
+  public static final String CONFIG_OVERRIDE_KEY_PREFIX = "vvm_config_override_key_";
 
   @Override
   public void onCreate(@Nullable Bundle savedInstanceState) {
diff --git a/java/com/android/voicemail/impl/settings/VisualVoicemailSettingsUtil.java b/java/com/android/voicemail/impl/settings/VisualVoicemailSettingsUtil.java
index 9ce32a9..ae526d1 100644
--- a/java/com/android/voicemail/impl/settings/VisualVoicemailSettingsUtil.java
+++ b/java/com/android/voicemail/impl/settings/VisualVoicemailSettingsUtil.java
@@ -16,6 +16,7 @@
 package com.android.voicemail.impl.settings;
 
 import android.content.Context;
+import android.support.annotation.VisibleForTesting;
 import android.telecom.PhoneAccountHandle;
 import com.android.dialer.common.Assert;
 import com.android.voicemail.VoicemailComponent;
@@ -28,7 +29,7 @@
 /** Save whether or not a particular account is enabled in shared to be retrieved later. */
 public class VisualVoicemailSettingsUtil {
 
-  private static final String IS_ENABLED_KEY = "is_enabled";
+  @VisibleForTesting public static final String IS_ENABLED_KEY = "is_enabled";
 
   public static void setEnabled(
       Context context, PhoneAccountHandle phoneAccount, boolean isEnabled) {
diff --git a/java/com/android/voicemail/impl/settings/VoicemailSettingsFragment.java b/java/com/android/voicemail/impl/settings/VoicemailSettingsFragment.java
index 4860649..e2ea725 100644
--- a/java/com/android/voicemail/impl/settings/VoicemailSettingsFragment.java
+++ b/java/com/android/voicemail/impl/settings/VoicemailSettingsFragment.java
@@ -151,6 +151,8 @@
         (PreferenceScreen) findPreference(getString(R.string.voicemail_advanced_settings_key));
     Intent advancedSettingsIntent = new Intent(TelephonyManager.ACTION_CONFIGURE_VOICEMAIL);
     advancedSettingsIntent.putExtra(TelephonyManager.EXTRA_HIDE_PUBLIC_SETTINGS, true);
+    advancedSettingsIntent.putExtra(
+        TelephonyManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle);
     advancedSettings.setIntent(advancedSettingsIntent);
     voicemailChangePinPreference.setOnPreferenceClickListener(
         new OnPreferenceClickListener() {
diff --git a/java/com/android/voicemail/impl/sms/LegacyModeSmsHandler.java b/java/com/android/voicemail/impl/sms/LegacyModeSmsHandler.java
index e902825..d55e3b5 100644
--- a/java/com/android/voicemail/impl/sms/LegacyModeSmsHandler.java
+++ b/java/com/android/voicemail/impl/sms/LegacyModeSmsHandler.java
@@ -101,6 +101,8 @@
       Intent launchVoicemailSettingsIntent =
           new Intent(TelephonyManager.ACTION_CONFIGURE_VOICEMAIL);
       launchVoicemailSettingsIntent.putExtra(TelephonyManager.EXTRA_HIDE_PUBLIC_SETTINGS, true);
+      launchVoicemailSettingsIntent.putExtra(
+          TelephonyManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle);
 
       launchVoicemailSettingsPendingIntent =
           PendingIntent.getActivity(
diff --git a/java/com/android/voicemail/impl/transcribe/TranscriptionService.java b/java/com/android/voicemail/impl/transcribe/TranscriptionService.java
index 2ca16fb..b733928 100644
--- a/java/com/android/voicemail/impl/transcribe/TranscriptionService.java
+++ b/java/com/android/voicemail/impl/transcribe/TranscriptionService.java
@@ -49,6 +49,8 @@
   private JobParameters jobParameters;
   private TranscriptionClientFactory clientFactory;
   private TranscriptionConfigProvider configProvider;
+  private TranscriptionTask activeTask;
+  private boolean stopped;
 
   /** Callback used by a task to indicate it has finished processing its work item */
   interface JobCallback {
@@ -134,8 +136,14 @@
   @MainThread
   public boolean onStopJob(JobParameters params) {
     Assert.isMainThread();
-    LogUtil.enterBlock("TranscriptionService.onStopJob");
-    cleanup();
+    LogUtil.i("TranscriptionService.onStopJob", "params: " + params);
+    stopped = true;
+    Logger.get(this).logImpression(DialerImpression.Type.VVM_TRANSCRIPTION_JOB_STOPPED);
+    if (activeTask != null) {
+      LogUtil.i("TranscriptionService.onStopJob", "cancelling active task");
+      activeTask.cancel();
+      Logger.get(this).logImpression(DialerImpression.Type.VVM_TRANSCRIPTION_TASK_CANCELLED);
+    }
     return true;
   }
 
@@ -161,15 +169,20 @@
   @MainThread
   private boolean checkForWork() {
     Assert.isMainThread();
+    if (stopped) {
+      LogUtil.i("TranscriptionService.checkForWork", "stopped");
+      return false;
+    }
     JobWorkItem workItem = jobParameters.dequeueWork();
     if (workItem != null) {
-      TranscriptionTask task =
+      Assert.checkState(activeTask == null);
+      activeTask =
           configProvider.shouldUseSyncApi()
               ? new TranscriptionTaskSync(
                   this, new Callback(), workItem, getClientFactory(), configProvider)
               : new TranscriptionTaskAsync(
                   this, new Callback(), workItem, getClientFactory(), configProvider);
-      getExecutorService().execute(task);
+      getExecutorService().execute(activeTask);
       return true;
     } else {
       return false;
@@ -196,8 +209,13 @@
     public void onWorkCompleted(JobWorkItem completedWorkItem) {
       Assert.isMainThread();
       LogUtil.i("TranscriptionService.Callback.onWorkCompleted", completedWorkItem.toString());
-      jobParameters.completeWork(completedWorkItem);
-      checkForWork();
+      activeTask = null;
+      if (stopped) {
+        LogUtil.i("TranscriptionService.Callback.onWorkCompleted", "stopped");
+      } else {
+        jobParameters.completeWork(completedWorkItem);
+        checkForWork();
+      }
     }
   }
 
diff --git a/java/com/android/voicemail/impl/transcribe/TranscriptionTask.java b/java/com/android/voicemail/impl/transcribe/TranscriptionTask.java
index fbab076..60b97da 100644
--- a/java/com/android/voicemail/impl/transcribe/TranscriptionTask.java
+++ b/java/com/android/voicemail/impl/transcribe/TranscriptionTask.java
@@ -19,8 +19,10 @@
 import android.app.job.JobWorkItem;
 import android.content.Context;
 import android.net.Uri;
+import android.support.annotation.MainThread;
 import android.text.TextUtils;
 import android.util.Pair;
+import com.android.dialer.common.Assert;
 import com.android.dialer.common.concurrent.ThreadUtil;
 import com.android.dialer.compat.android.provider.VoicemailCompat;
 import com.android.dialer.logging.DialerImpression;
@@ -64,6 +66,7 @@
   protected final TranscriptionConfigProvider configProvider;
   protected ByteString audioData;
   protected AudioFormat encoding;
+  protected volatile boolean cancelled;
 
   static final String AMR_PREFIX = "#!AMR\n";
 
@@ -87,6 +90,13 @@
     databaseHelper = new TranscriptionDbHelper(context, voicemailUri);
   }
 
+  @MainThread
+  void cancel() {
+    Assert.isMainThread();
+    VvmLog.i(TAG, "cancel");
+    cancelled = true;
+  }
+
   @Override
   public void run() {
     VvmLog.i(TAG, "run");
@@ -144,7 +154,11 @@
               .logImpression(DialerImpression.Type.VVM_TRANSCRIPTION_RESPONSE_EXPIRED);
           break;
         default:
-          updateTranscriptionAndState(transcript, VoicemailCompat.TRANSCRIPTION_FAILED);
+          updateTranscriptionAndState(
+              transcript,
+              cancelled
+                  ? VoicemailCompat.TRANSCRIPTION_NOT_STARTED
+                  : VoicemailCompat.TRANSCRIPTION_FAILED);
           Logger.get(context).logImpression(DialerImpression.Type.VVM_TRANSCRIPTION_RESPONSE_EMPTY);
           break;
       }
@@ -155,6 +169,11 @@
     VvmLog.i(TAG, "sendRequest");
     TranscriptionClient client = clientFactory.getClient();
     for (int i = 0; i < configProvider.getMaxTranscriptionRetries(); i++) {
+      if (cancelled) {
+        VvmLog.i(TAG, "sendRequest, cancelled");
+        return null;
+      }
+
       VvmLog.i(TAG, "sendRequest, try: " + (i + 1));
       if (i == 0) {
         Logger.get(context).logImpression(getRequestSentImpression());
@@ -163,7 +182,10 @@
       }
 
       TranscriptionResponse response = request.getResponse(client);
-      if (response.hasRecoverableError()) {
+      if (cancelled) {
+        VvmLog.i(TAG, "sendRequest, cancelled");
+        return null;
+      } else if (response.hasRecoverableError()) {
         Logger.get(context)
             .logImpression(DialerImpression.Type.VVM_TRANSCRIPTION_RESPONSE_RECOVERABLE_ERROR);
         backoff(i);
@@ -187,7 +209,7 @@
     try {
       Thread.sleep(millis);
     } catch (InterruptedException e) {
-      VvmLog.w(TAG, "interrupted");
+      VvmLog.e(TAG, "interrupted", e);
       Thread.currentThread().interrupt();
     }
   }
diff --git a/java/com/android/voicemail/impl/transcribe/TranscriptionTaskAsync.java b/java/com/android/voicemail/impl/transcribe/TranscriptionTaskAsync.java
index 3c41aef..930d7f1 100644
--- a/java/com/android/voicemail/impl/transcribe/TranscriptionTaskAsync.java
+++ b/java/com/android/voicemail/impl/transcribe/TranscriptionTaskAsync.java
@@ -62,7 +62,10 @@
         (TranscriptionResponseAsync)
             sendRequest((client) -> client.sendUploadRequest(getUploadRequest()));
 
-    if (uploadResponse == null) {
+    if (cancelled) {
+      VvmLog.i(TAG, "getTranscription, cancelled.");
+      return new Pair<>(null, TranscriptionStatus.FAILED_NO_RETRY);
+    } else if (uploadResponse == null) {
       VvmLog.i(TAG, "getTranscription, failed to upload voicemail.");
       return new Pair<>(null, TranscriptionStatus.FAILED_NO_RETRY);
     } else {
@@ -87,10 +90,17 @@
     VvmLog.i(TAG, "pollForTranscription");
     GetTranscriptRequest request = getGetTranscriptRequest(uploadResponse);
     for (int i = 0; i < configProvider.getMaxGetTranscriptPolls(); i++) {
+      if (cancelled) {
+        VvmLog.i(TAG, "pollForTranscription, cancelled.");
+        return new Pair<>(null, TranscriptionStatus.FAILED_NO_RETRY);
+      }
       GetTranscriptResponseAsync response =
           (GetTranscriptResponseAsync)
               sendRequest((client) -> client.sendGetTranscriptRequest(request));
-      if (response == null) {
+      if (cancelled) {
+        VvmLog.i(TAG, "pollForTranscription, cancelled.");
+        return new Pair<>(null, TranscriptionStatus.FAILED_NO_RETRY);
+      } else if (response == null) {
         VvmLog.i(TAG, "pollForTranscription, no transcription result.");
       } else if (response.isTranscribing()) {
         VvmLog.i(TAG, "pollForTranscription, poll count: " + (i + 1));
diff --git a/java/com/android/voicemail/stub/StubVoicemailClient.java b/java/com/android/voicemail/stub/StubVoicemailClient.java
index 9929503..c2c7a6d 100644
--- a/java/com/android/voicemail/stub/StubVoicemailClient.java
+++ b/java/com/android/voicemail/stub/StubVoicemailClient.java
@@ -77,7 +77,9 @@
 
   @Override
   public Intent getSetPinIntent(Context context, PhoneAccountHandle phoneAccountHandle) {
-    return new Intent(TelephonyManager.ACTION_CONFIGURE_VOICEMAIL);
+    Intent intent = new Intent(TelephonyManager.ACTION_CONFIGURE_VOICEMAIL);
+    intent.putExtra(TelephonyManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle);
+    return intent;
   }
 
   @Override