Log swiping and clicking for switching tabs.

If it's a swipe, onPageScrolled() is called several times before onPageScrollStateChanged(SCROLL_STATE_SETTLING) and onPageSelected().
If it's a click, only onPageScrollStateChanged(SCROLL_STATE_SETTLING) is called before onPageSelected().
And onPageScrollStateChanged(SCROLL_STATE_SETTLING) will not be called if user don't switch to a new tab.
We use the difference to tell if user switching tabs by swiping or clicking.

Test: DialtactsActivityTest
PiperOrigin-RevId: 168403148
Change-Id: Iaaf84ab9c4955d0bc2c1e9857ba59fd37b3984af
diff --git a/java/com/android/bubble/Bubble.java b/java/com/android/bubble/Bubble.java
index d83e284..9abfa43 100644
--- a/java/com/android/bubble/Bubble.java
+++ b/java/com/android/bubble/Bubble.java
@@ -99,6 +99,7 @@
   private boolean expanded;
   private boolean textShowing;
   private boolean hideAfterText;
+  private CharSequence textAfterShow;
   private int collapseEndAction;
 
   @VisibleForTesting ViewHolder viewHolder;
@@ -304,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();
@@ -380,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);
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/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/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/logging/dialer_impression.proto b/java/com/android/dialer/logging/dialer_impression.proto
index 154460c..f273a36 100644
--- a/java/com/android/dialer/logging/dialer_impression.proto
+++ b/java/com/android/dialer/logging/dialer_impression.proto
@@ -537,5 +537,15 @@
     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/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/incallui/videotech/ims/ImsVideoCallCallback.java b/java/com/android/incallui/videotech/ims/ImsVideoCallCallback.java
index b839293..49170b8 100644
--- a/java/com/android/incallui/videotech/ims/ImsVideoCallCallback.java
+++ b/java/com/android/incallui/videotech/ims/ImsVideoCallCallback.java
@@ -66,10 +66,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();
+      }
     }
   }