Merge changes I84ec0ac5,I2b92e1e0,Ic30fa6c6

* changes:
  Move Duo related constants out of interface
  Dismissing location prompt now actually prevents it from reshowing.
  Add GSM conference calling to simulator.
diff --git a/java/com/android/dialer/app/calllog/CallLogAdapter.java b/java/com/android/dialer/app/calllog/CallLogAdapter.java
index 679901e..016bce3 100644
--- a/java/com/android/dialer/app/calllog/CallLogAdapter.java
+++ b/java/com/android/dialer/app/calllog/CallLogAdapter.java
@@ -75,6 +75,7 @@
 import com.android.dialer.configprovider.ConfigProviderBindings;
 import com.android.dialer.duo.Duo;
 import com.android.dialer.duo.DuoComponent;
+import com.android.dialer.duo.DuoConstants;
 import com.android.dialer.duo.DuoListener;
 import com.android.dialer.enrichedcall.EnrichedCallCapabilities;
 import com.android.dialer.enrichedcall.EnrichedCallComponent;
@@ -399,11 +400,7 @@
           if (intentProvider == null) {
             return false;
           }
-          String packageName = DuoComponent.get(mActivity).getDuo().getPackageName();
-          if (packageName == null) {
-            return false;
-          }
-          return packageName.equals(intentProvider.getIntent(mActivity).getPackage());
+          return DuoConstants.PACKAGE_NAME.equals(intentProvider.getIntent(mActivity).getPackage());
         }
       };
 
@@ -697,7 +694,7 @@
 
   @Override
   protected void addGroups(Cursor cursor) {
-    mCallLogGroupBuilder.addGroups(cursor, mActivity);
+    mCallLogGroupBuilder.addGroups(cursor);
   }
 
   @Override
@@ -981,11 +978,9 @@
               .setFeatures(cursor.getInt(CallLogQuery.FEATURES));
 
       String phoneAccountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME);
-      if (getDuo().getPhoneAccountComponentName() != null
-          && getDuo()
-              .getPhoneAccountComponentName()
-              .flattenToString()
-              .equals(phoneAccountComponentName)) {
+      if (DuoConstants.PHONE_ACCOUNT_COMPONENT_NAME
+          .flattenToString()
+          .equals(phoneAccountComponentName)) {
         entry.setIsDuoCall(true);
       }
 
diff --git a/java/com/android/dialer/app/calllog/CallLogGroupBuilder.java b/java/com/android/dialer/app/calllog/CallLogGroupBuilder.java
index 57a8be7..513c8aa 100644
--- a/java/com/android/dialer/app/calllog/CallLogGroupBuilder.java
+++ b/java/com/android/dialer/app/calllog/CallLogGroupBuilder.java
@@ -16,7 +16,6 @@
 
 package com.android.dialer.app.calllog;
 
-import android.content.Context;
 import android.database.Cursor;
 import android.os.Build.VERSION;
 import android.os.Build.VERSION_CODES;
@@ -74,7 +73,7 @@
    *
    * @see GroupingListAdapter#addGroups(Cursor)
    */
-  public void addGroups(Cursor cursor, Context context) {
+  public void addGroups(Cursor cursor) {
     final int count = cursor.getCount();
     if (count == 0) {
       return;
@@ -99,7 +98,7 @@
     int groupFeatures = cursor.getInt(CallLogQuery.FEATURES);
     int groupCallbackAction =
         CallbackActionHelper.getCallbackAction(
-            groupNumber, groupFeatures, groupAccountComponentName, context);
+            groupNumber, groupFeatures, groupAccountComponentName);
     mGroupCreator.setCallbackAction(firstRowId, groupCallbackAction);
 
     // Instantiate other group values to those of the first call in the cursor.
@@ -134,7 +133,7 @@
       accountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME);
       accountId = cursor.getString(CallLogQuery.ACCOUNT_ID);
       callbackAction =
-          CallbackActionHelper.getCallbackAction(number, features, accountComponentName, context);
+          CallbackActionHelper.getCallbackAction(number, features, accountComponentName);
 
       final boolean isSameNumber = equalNumbers(groupNumber, number);
       final boolean isSamePostDialDigits = groupPostDialDigits.equals(numberPostDialDigits);
diff --git a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
index f0852bd..d5dfb06 100644
--- a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
+++ b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
@@ -27,7 +27,6 @@
 import android.provider.CallLog.Calls;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.support.annotation.IntDef;
-import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.annotation.VisibleForTesting;
 import android.support.v7.widget.CardView;
@@ -75,8 +74,7 @@
 import com.android.dialer.contactphoto.ContactPhotoManager;
 import com.android.dialer.dialercontact.DialerContact;
 import com.android.dialer.dialercontact.SimDetails;
-import com.android.dialer.duo.Duo;
-import com.android.dialer.duo.DuoComponent;
+import com.android.dialer.duo.DuoConstants;
 import com.android.dialer.lettertile.LetterTileDrawable;
 import com.android.dialer.lettertile.LetterTileDrawable.ContactType;
 import com.android.dialer.logging.ContactSource;
@@ -746,7 +744,7 @@
 
   private boolean showDuoPrimaryButton() {
     return accountHandle != null
-        && accountHandle.getComponentName().equals(getDuo().getPhoneAccountComponentName())
+        && accountHandle.getComponentName().equals(DuoConstants.PHONE_ACCOUNT_COMPONENT_NAME)
         && duoReady;
   }
 
@@ -961,7 +959,7 @@
       // We check to see if we are starting a Duo intent. The reason is Duo
       // intents need to be started using startActivityForResult instead of the usual startActivity
       String packageName = intent.getPackage();
-      if (packageName != null && packageName.equals(getDuo().getPackageName())) {
+      if (DuoConstants.PACKAGE_NAME.equals(packageName)) {
         Logger.get(mContext)
             .logImpression(DialerImpression.Type.LIGHTBRINGER_VIDEO_REQUESTED_FROM_CALL_LOG);
         if (isNonContactEntry(info)) {
@@ -1125,11 +1123,6 @@
     return callDetailsEntries;
   }
 
-  @NonNull
-  private Duo getDuo() {
-    return DuoComponent.get(mContext).getDuo();
-  }
-
   @Override
   public void onCreateContextMenu(
       final ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
diff --git a/java/com/android/dialer/app/calllog/MissedCallNotifier.java b/java/com/android/dialer/app/calllog/MissedCallNotifier.java
index f50751e..fff68d4 100644
--- a/java/com/android/dialer/app/calllog/MissedCallNotifier.java
+++ b/java/com/android/dialer/app/calllog/MissedCallNotifier.java
@@ -53,6 +53,7 @@
 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.duo.DuoConstants;
 import com.android.dialer.enrichedcall.FuzzyPhoneNumberMatcher;
 import com.android.dialer.notification.DialerNotificationManager;
 import com.android.dialer.notification.NotificationChannelId;
@@ -253,6 +254,7 @@
     if (newCalls == null) {
       return;
     }
+
     TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
     Iterator<NewCall> iterator = newCalls.iterator();
     while (iterator.hasNext()) {
@@ -269,6 +271,10 @@
       if (phoneAccount == null) {
         continue;
       }
+      if (DuoConstants.PHONE_ACCOUNT_HANDLE.equals(phoneAccountHandle)) {
+        iterator.remove();
+        continue;
+      }
       if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)) {
         LogUtil.i(
             "MissedCallNotifier.removeSelfManagedCalls",
diff --git a/java/com/android/dialer/calllogutils/CallbackActionHelper.java b/java/com/android/dialer/calllogutils/CallbackActionHelper.java
index 3049943..1e219f1 100644
--- a/java/com/android/dialer/calllogutils/CallbackActionHelper.java
+++ b/java/com/android/dialer/calllogutils/CallbackActionHelper.java
@@ -16,12 +16,10 @@
 
 package com.android.dialer.calllogutils;
 
-import android.content.Context;
 import android.provider.CallLog.Calls;
 import android.support.annotation.IntDef;
 import android.text.TextUtils;
-import com.android.dialer.duo.Duo;
-import com.android.dialer.duo.DuoComponent;
+import com.android.dialer.duo.DuoConstants;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
@@ -45,12 +43,11 @@
    * @param features Value of features in column {@link android.provider.CallLog.Calls#FEATURES}.
    * @param phoneAccountComponentName Account name in column {@link
    *     android.provider.CallLog.Calls#PHONE_ACCOUNT_COMPONENT_NAME}.
-   * @param context The context in which the method is called.
    * @return One of the values in {@link CallbackAction}
    */
   public static @CallbackAction int getCallbackAction(
-      String number, int features, String phoneAccountComponentName, Context context) {
-    return getCallbackAction(number, features, isDuoCall(phoneAccountComponentName, context));
+      String number, int features, String phoneAccountComponentName) {
+    return getCallbackAction(number, features, isDuoCall(phoneAccountComponentName));
   }
 
   /**
@@ -78,12 +75,9 @@
     return CallbackAction.VOICE;
   }
 
-  private static boolean isDuoCall(String phoneAccountComponentName, Context context) {
-    Duo lightBringer = DuoComponent.get(context).getDuo();
-    return lightBringer.getPhoneAccountComponentName() != null
-        && lightBringer
-            .getPhoneAccountComponentName()
-            .flattenToString()
-            .equals(phoneAccountComponentName);
+  private static boolean isDuoCall(String phoneAccountComponentName) {
+    return DuoConstants.PHONE_ACCOUNT_COMPONENT_NAME
+        .flattenToString()
+        .equals(phoneAccountComponentName);
   }
 }
diff --git a/java/com/android/dialer/duo/Duo.java b/java/com/android/dialer/duo/Duo.java
index ec07ad4..839c1d3 100644
--- a/java/com/android/dialer/duo/Duo.java
+++ b/java/com/android/dialer/duo/Duo.java
@@ -16,7 +16,6 @@
 
 package com.android.dialer.duo;
 
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.support.annotation.MainThread;
@@ -24,7 +23,6 @@
 import android.support.annotation.Nullable;
 import android.support.annotation.StringRes;
 import android.telecom.Call;
-import android.telecom.PhoneAccountHandle;
 import com.google.auto.value.AutoValue;
 import com.google.common.base.Optional;
 import java.util.List;
@@ -55,18 +53,6 @@
   @MainThread
   void unregisterListener(@NonNull DuoListener listener);
 
-  @Nullable
-  @MainThread
-  ComponentName getPhoneAccountComponentName();
-
-  @Nullable
-  @MainThread
-  PhoneAccountHandle getPhoneAccountHandle();
-
-  @Nullable
-  @MainThread
-  String getPackageName();
-
   @StringRes
   @MainThread
   int getOutgoingCallTypeText();
diff --git a/java/com/android/dialer/duo/DuoConstants.java b/java/com/android/dialer/duo/DuoConstants.java
new file mode 100644
index 0000000..50254ee
--- /dev/null
+++ b/java/com/android/dialer/duo/DuoConstants.java
@@ -0,0 +1,38 @@
+/*
+ * 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.duo;
+
+import android.content.ComponentName;
+import android.telecom.PhoneAccountHandle;
+
+/** Constants to reference the Duo application. */
+public final class DuoConstants {
+  public static final String PACKAGE_NAME = "com.google.android.apps.tachyon";
+
+  public static final String CONNECTION_SERVICE =
+      "com.google.android.apps.tachyon.telecom.TachyonTelecomConnectionService";
+
+  public static final String PHONE_ACCOUNT_ID = "0";
+
+  public static final ComponentName PHONE_ACCOUNT_COMPONENT_NAME =
+      new ComponentName(PACKAGE_NAME, CONNECTION_SERVICE);
+
+  public static final PhoneAccountHandle PHONE_ACCOUNT_HANDLE =
+      new PhoneAccountHandle(PHONE_ACCOUNT_COMPONENT_NAME, PHONE_ACCOUNT_ID);
+
+  private DuoConstants() {}
+}
diff --git a/java/com/android/dialer/duo/stub/DuoStub.java b/java/com/android/dialer/duo/stub/DuoStub.java
index 99f03ad..82b9c79 100644
--- a/java/com/android/dialer/duo/stub/DuoStub.java
+++ b/java/com/android/dialer/duo/stub/DuoStub.java
@@ -16,7 +16,6 @@
 
 package com.android.dialer.duo.stub;
 
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.support.annotation.MainThread;
@@ -24,7 +23,6 @@
 import android.support.annotation.Nullable;
 import android.support.annotation.StringRes;
 import android.telecom.Call;
-import android.telecom.PhoneAccountHandle;
 import com.android.dialer.common.Assert;
 import com.android.dialer.duo.Duo;
 import com.android.dialer.duo.DuoListener;
@@ -95,24 +93,6 @@
     Assert.isNotNull(listener);
   }
 
-  @Nullable
-  @Override
-  public ComponentName getPhoneAccountComponentName() {
-    return null;
-  }
-
-  @Nullable
-  @Override
-  public PhoneAccountHandle getPhoneAccountHandle() {
-    return null;
-  }
-
-  @Nullable
-  @Override
-  public String getPackageName() {
-    return null;
-  }
-
   @StringRes
   @Override
   public int getOutgoingCallTypeText() {
diff --git a/java/com/android/dialer/searchfragment/list/NewSearchFragment.java b/java/com/android/dialer/searchfragment/list/NewSearchFragment.java
index 393b07a..6115c2f 100644
--- a/java/com/android/dialer/searchfragment/list/NewSearchFragment.java
+++ b/java/com/android/dialer/searchfragment/list/NewSearchFragment.java
@@ -345,7 +345,7 @@
     if (!PermissionsUtil.hasLocationPermissions(getContext())
         && !DialerUtils.getDefaultSharedPreferenceForDeviceProtectedStorageContext(getContext())
             .getBoolean(KEY_LOCATION_PROMPT_DISMISSED, false)) {
-      if (adapter != null && isRegularSearch()) {
+      if (adapter != null && isRegularSearch() && !hasBeenDismissed()) {
         adapter.showLocationPermissionRequest(
             v -> requestLocationPermission(), v -> dismissLocationPermission());
       }
@@ -372,7 +372,8 @@
     requestPermissions(deniedPermissions, LOCATION_PERMISSION_REQUEST_CODE);
   }
 
-  private void dismissLocationPermission() {
+  @VisibleForTesting
+  public void dismissLocationPermission() {
     PreferenceManager.getDefaultSharedPreferences(getContext())
         .edit()
         .putBoolean(KEY_LOCATION_PROMPT_DISMISSED, true)
@@ -380,6 +381,11 @@
     adapter.hideLocationPermissionRequest();
   }
 
+  private boolean hasBeenDismissed() {
+    return PreferenceManager.getDefaultSharedPreferences(getContext())
+        .getBoolean(KEY_LOCATION_PROMPT_DISMISSED, false);
+  }
+
   @Override
   public void onResume() {
     super.onResume();
diff --git a/java/com/android/dialer/searchfragment/list/SearchAdapter.java b/java/com/android/dialer/searchfragment/list/SearchAdapter.java
index 29e4e23..949c2a2 100644
--- a/java/com/android/dialer/searchfragment/list/SearchAdapter.java
+++ b/java/com/android/dialer/searchfragment/list/SearchAdapter.java
@@ -187,7 +187,7 @@
     this.allowClickListener = Assert.isNotNull(allowClickListener);
     this.dismissClickListener = Assert.isNotNull(dismissClickListener);
     if (searchCursorManager.showLocationPermissionRequest(true)) {
-      notifyItemRemoved(0);
+      notifyItemInserted(0);
     }
   }
 
diff --git a/java/com/android/dialer/simulator/Simulator.java b/java/com/android/dialer/simulator/Simulator.java
index f753e5f..4812fa5 100644
--- a/java/com/android/dialer/simulator/Simulator.java
+++ b/java/com/android/dialer/simulator/Simulator.java
@@ -30,6 +30,16 @@
 
   ActionProvider getActionProvider(Context context);
 
+  /** The type of conference to emulate. */
+  // TODO(b/67785540): add VoLTE and CDMA conference call
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({
+    CONFERENCE_TYPE_GSM,
+  })
+  @interface ConferenceType {}
+
+  static final int CONFERENCE_TYPE_GSM = 1;
+
   /** Information about a connection event. */
   public static class Event {
     /** The type of connection event. */
@@ -44,6 +54,11 @@
       STATE_CHANGE,
       DTMF,
       SESSION_MODIFY_REQUEST,
+      CALL_AUDIO_STATE_CHANGED,
+      CONNECTION_ADDED,
+      MERGE,
+      SEPARATE,
+      SWAP,
     })
     public @interface Type {}
 
@@ -56,6 +71,11 @@
     public static final int STATE_CHANGE = 6;
     public static final int DTMF = 7;
     public static final int SESSION_MODIFY_REQUEST = 8;
+    public static final int CALL_AUDIO_STATE_CHANGED = 9;
+    public static final int CONNECTION_ADDED = 10;
+    public static final int MERGE = 11;
+    public static final int SEPARATE = 12;
+    public static final int SWAP = 13;
 
     @Type public final int type;
     /** Holds event specific information. For example, for DTMF this could be the keycode. */
diff --git a/java/com/android/dialer/simulator/impl/SimulatorConference.java b/java/com/android/dialer/simulator/impl/SimulatorConference.java
new file mode 100644
index 0000000..7468b56
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/SimulatorConference.java
@@ -0,0 +1,168 @@
+/*
+ * 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.support.annotation.NonNull;
+import android.telecom.CallAudioState;
+import android.telecom.Conference;
+import android.telecom.Connection;
+import android.telecom.PhoneAccountHandle;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.simulator.Simulator;
+import com.android.dialer.simulator.Simulator.Event;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents a conference call. When a user merges two phone calls we create an instance of this
+ * conference object and add it to the connection service. All operations such as hold and DTMF are
+ * then performed on this object.
+ */
+public final class SimulatorConference extends Conference implements SimulatorConnection.Listener {
+  static final int PROPERTY_GENERIC_CONFERENCE = 1 << 1;
+
+  private final List<Listener> listeners = new ArrayList<>();
+  private final List<Event> events = new ArrayList<>();
+  private final int conferenceType;
+
+  private SimulatorConference(
+      PhoneAccountHandle handle, @Simulator.ConferenceType int conferenceType) {
+    super(handle);
+    this.conferenceType = conferenceType;
+    setActive();
+  }
+
+  static SimulatorConference newGsmConference(PhoneAccountHandle handle) {
+    SimulatorConference simulatorConference =
+        new SimulatorConference(handle, Simulator.CONFERENCE_TYPE_GSM);
+    simulatorConference.setConnectionCapabilities(
+        Connection.CAPABILITY_MUTE
+            | Connection.CAPABILITY_SUPPORT_HOLD
+            | Connection.CAPABILITY_HOLD
+            | Connection.CAPABILITY_MANAGE_CONFERENCE);
+    return simulatorConference;
+  }
+
+  public void addListener(@NonNull Listener listener) {
+    listeners.add(Assert.isNotNull(listener));
+  }
+
+  public void removeListener(@NonNull Listener listener) {
+    listeners.remove(Assert.isNotNull(listener));
+  }
+
+  @NonNull
+  public List<Event> getEvents() {
+    return events;
+  }
+
+  @Override
+  public void onCallAudioStateChanged(CallAudioState state) {
+    LogUtil.enterBlock("SimulatorConference.onCallAudioStateChanged");
+    onEvent(new Event(Event.CALL_AUDIO_STATE_CHANGED));
+  }
+
+  @Override
+  public void onConnectionAdded(Connection connection) {
+    LogUtil.enterBlock("SimulatorConference.onConnectionAdded");
+    onEvent(
+        new Event(
+            Event.CONNECTION_ADDED, SimulatorSimCallManager.getConnectionTag(connection), null));
+    ((SimulatorConnection) connection).addListener(this);
+  }
+
+  @Override
+  public void onDisconnect() {
+    LogUtil.enterBlock("SimulatorConference.onDisconnect");
+    onEvent(new Event(Event.DISCONNECT));
+  }
+
+  @Override
+  public void onHold() {
+    LogUtil.enterBlock("SimulatorConference.onHold");
+    onEvent(new Event(Event.HOLD));
+  }
+
+  @Override
+  public void onMerge(Connection connection) {
+    LogUtil.i("SimulatorConference.onMerge", "connection: " + connection);
+    onEvent(new Event(Event.MERGE, SimulatorSimCallManager.getConnectionTag(connection), null));
+  }
+
+  @Override
+  public void onMerge() {
+    LogUtil.enterBlock("SimulatorConference.onMerge");
+    onEvent(new Event(Event.MERGE));
+  }
+
+  @Override
+  public void onPlayDtmfTone(char c) {
+    LogUtil.enterBlock("SimulatorConference.onPlayDtmfTone");
+    onEvent(new Event(Event.DTMF, Character.toString(c), null));
+  }
+
+  @Override
+  public void onSeparate(Connection connection) {
+    LogUtil.i("SimulatorConference.onSeparate", "connection: " + connection);
+    onEvent(new Event(Event.SEPARATE, SimulatorSimCallManager.getConnectionTag(connection), null));
+  }
+
+  @Override
+  public void onSwap() {
+    LogUtil.enterBlock("SimulatorConference.onSwap");
+    onEvent(new Event(Event.SWAP));
+  }
+
+  @Override
+  public void onUnhold() {
+    LogUtil.enterBlock("SimulatorConference.onUnhold");
+    onEvent(new Event(Event.UNHOLD));
+  }
+
+  @Override
+  public void onEvent(@NonNull SimulatorConnection connection, @NonNull Event event) {
+    if (conferenceType == Simulator.CONFERENCE_TYPE_GSM) {
+      onGsmEvent(connection, event);
+    }
+  }
+
+  private void onGsmEvent(@NonNull SimulatorConnection connection, @NonNull Event event) {
+    if (event.type == Event.STATE_CHANGE
+        && Connection.stateToString(Connection.STATE_DISCONNECTED).equals(event.data2)) {
+      removeConnection(connection);
+      connection.removeListener(this);
+      if (getConnections().size() <= 1) {
+        // When only one connection exists, it's not conference call anymore
+        setDisconnected(connection.getDisconnectCause());
+        destroy();
+      }
+    }
+  }
+
+  void onEvent(@NonNull Event event) {
+    events.add(Assert.isNotNull(event));
+    for (Listener listener : new ArrayList<>(listeners)) {
+      listener.onEvent(this, event);
+    }
+  }
+
+  /** Callback for when a new event arrives. */
+  public interface Listener {
+    void onEvent(@NonNull SimulatorConference conference, @NonNull Event event);
+  }
+}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorConferenceCreator.java b/java/com/android/dialer/simulator/impl/SimulatorConferenceCreator.java
new file mode 100644
index 0000000..838b58d
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/SimulatorConferenceCreator.java
@@ -0,0 +1,229 @@
+/*
+ * 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.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.telecom.Conferenceable;
+import android.telecom.Connection;
+import android.telecom.DisconnectCause;
+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;
+import com.android.dialer.simulator.Simulator.Event;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/** Creates a conference with a given number of participants. */
+final class SimulatorConferenceCreator
+    implements SimulatorConnectionService.Listener,
+        SimulatorConnection.Listener,
+        SimulatorConference.Listener {
+  private static final String EXTRA_CALL_COUNT = "call_count";
+
+  @NonNull private final Context context;
+  @NonNull private final List<String> connectionTags = new ArrayList<>();
+  @Simulator.ConferenceType private final int conferenceType;
+
+  public SimulatorConferenceCreator(
+      @NonNull Context context, @Simulator.ConferenceType int conferenceType) {
+    this.context = Assert.isNotNull(context);
+    this.conferenceType = conferenceType;
+  }
+
+  void start(int callCount) {
+    SimulatorConnectionService.addListener(this);
+    addNextCall(callCount);
+  }
+
+  private void addNextCall(int callCount) {
+    LogUtil.i("SimulatorConferenceCreator.addNextIncomingCall", "callCount: " + callCount);
+    if (callCount <= 0) {
+      LogUtil.i("SimulatorConferenceCreator.addNextCall", "done adding calls");
+      return;
+    }
+
+    String callerId = String.format(Locale.US, "+1-650-234%04d", callCount);
+    Bundle extras = new Bundle();
+    extras.putInt(EXTRA_CALL_COUNT, callCount - 1);
+    connectionTags.add(
+        SimulatorSimCallManager.addNewIncomingCall(context, callerId, false /* isVideo */, extras));
+  }
+
+  @Override
+  public void onNewIncomingConnection(@NonNull SimulatorConnection connection) {
+    if (!isMyConnection(connection)) {
+      LogUtil.i("SimulatorConferenceCreator.onNewOutgoingConnection", "unknown connection");
+      return;
+    }
+
+    LogUtil.i("SimulatorConferenceCreator.onNewOutgoingConnection", "connection created");
+    connection.addListener(this);
+
+    // 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());
+
+    // Once the connection is active, go ahead and conference it and add the next call.
+    ThreadUtil.postDelayedOnUiThread(
+        () -> {
+          SimulatorConference conference = findCurrentConference();
+          if (conference == null) {
+            Assert.checkArgument(conferenceType == Simulator.CONFERENCE_TYPE_GSM);
+            conference =
+                SimulatorConference.newGsmConference(
+                    SimulatorSimCallManager.getSystemPhoneAccountHandle(context));
+            conference.addListener(this);
+            SimulatorConnectionService.getInstance().addConference(conference);
+          }
+          updateConferenceableConnections();
+          conference.addConnection(connection);
+          addNextCall(getCallCount(connection));
+        },
+        1000);
+  }
+
+  @Override
+  public void onNewOutgoingConnection(@NonNull SimulatorConnection connection) {}
+
+  /**
+   * This is called when the user clicks the merge button. We create the initial conference
+   * automatically but with this method we can let the user split and merge calls as desired.
+   */
+  @Override
+  public void onConference(
+      @NonNull SimulatorConnection connection1, @NonNull SimulatorConnection connection2) {
+    LogUtil.enterBlock("SimulatorConferenceCreator.onConference");
+    if (!isMyConnection(connection1) || !isMyConnection(connection2)) {
+      LogUtil.i("SimulatorConferenceCreator.onConference", "unknown connections, ignoring");
+      return;
+    }
+
+    if (connection1.getConference() != null) {
+      connection1.getConference().addConnection(connection2);
+    } else if (connection2.getConference() != null) {
+      connection2.getConference().addConnection(connection1);
+    } else {
+      Assert.checkArgument(conferenceType == Simulator.CONFERENCE_TYPE_GSM);
+      SimulatorConference conference =
+          SimulatorConference.newGsmConference(
+              SimulatorSimCallManager.getSystemPhoneAccountHandle(context));
+      conference.addConnection(connection1);
+      conference.addConnection(connection2);
+      conference.addListener(this);
+      SimulatorConnectionService.getInstance().addConference(conference);
+    }
+  }
+
+  private boolean isMyConnection(@NonNull Connection connection) {
+    for (String connectionTag : connectionTags) {
+      if (connection.getExtras().getBoolean(connectionTag)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private void updateConferenceableConnections() {
+    LogUtil.enterBlock("SimulatorConferenceCreator.updateConferenceableConnections");
+    for (String connectionTag : connectionTags) {
+      SimulatorConnection connection = SimulatorSimCallManager.findConnectionByTag(connectionTag);
+      List<Conferenceable> conferenceables = getMyConferenceables();
+      conferenceables.remove(connection);
+      conferenceables.remove(connection.getConference());
+      connection.setConferenceables(conferenceables);
+    }
+  }
+
+  private List<Conferenceable> getMyConferenceables() {
+    List<Conferenceable> conferenceables = new ArrayList<>();
+    for (String connectionTag : connectionTags) {
+      SimulatorConnection connection = SimulatorSimCallManager.findConnectionByTag(connectionTag);
+      conferenceables.add(connection);
+      if (connection.getConference() != null
+          && !conferenceables.contains(connection.getConference())) {
+        conferenceables.add(connection.getConference());
+      }
+    }
+    return conferenceables;
+  }
+
+  @Nullable
+  private SimulatorConference findCurrentConference() {
+    for (String connectionTag : connectionTags) {
+      SimulatorConnection connection = SimulatorSimCallManager.findConnectionByTag(connectionTag);
+      if (connection.getConference() != null) {
+        return (SimulatorConference) connection.getConference();
+      }
+    }
+    return null;
+  }
+
+  private static int getCallCount(@NonNull Connection connection) {
+    return connection.getExtras().getInt(EXTRA_CALL_COUNT);
+  }
+
+  @Override
+  public void onEvent(@NonNull SimulatorConnection connection, @NonNull Event event) {
+    switch (event.type) {
+      case Event.NONE:
+        throw Assert.createIllegalStateFailException();
+      case Event.HOLD:
+        connection.setOnHold();
+        break;
+      case Event.UNHOLD:
+        connection.setActive();
+        break;
+      case Event.DISCONNECT:
+        connection.setDisconnected(new DisconnectCause(DisconnectCause.LOCAL));
+        break;
+      default:
+        LogUtil.i(
+            "SimulatorConferenceCreator.onEvent", "unexpected conference event: " + event.type);
+        break;
+    }
+  }
+
+  @Override
+  public void onEvent(@NonNull SimulatorConference conference, @NonNull Event event) {
+    switch (event.type) {
+      case Event.MERGE:
+        int capabilities = conference.getConnectionCapabilities();
+        capabilities |= Connection.CAPABILITY_SWAP_CONFERENCE;
+        conference.setConnectionCapabilities(capabilities);
+        break;
+      case Event.SEPARATE:
+        SimulatorConnection connectionToRemove =
+            SimulatorSimCallManager.findConnectionByTag(event.data1);
+        conference.removeConnection(connectionToRemove);
+        break;
+      case Event.DISCONNECT:
+        for (Connection connection : new ArrayList<>(conference.getConnections())) {
+          connection.setDisconnected(new DisconnectCause(DisconnectCause.LOCAL));
+        }
+        break;
+      default:
+        LogUtil.i(
+            "SimulatorConferenceCreator.onEvent", "unexpected conference event: " + event.type);
+        break;
+    }
+  }
+}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorConnection.java b/java/com/android/dialer/simulator/impl/SimulatorConnection.java
index 70c1095..e4a34b5 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorConnection.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorConnection.java
@@ -41,7 +41,9 @@
         CAPABILITY_MUTE
             | CAPABILITY_SUPPORT_HOLD
             | CAPABILITY_HOLD
-            | CAPABILITY_CAN_UPGRADE_TO_VIDEO);
+            | CAPABILITY_CAN_UPGRADE_TO_VIDEO
+            | CAPABILITY_DISCONNECT_FROM_CONFERENCE
+            | CAPABILITY_SEPARATE_FROM_CONFERENCE);
     setVideoProvider(new SimulatorVideoProvider(context, this));
   }
 
@@ -108,7 +110,7 @@
 
   void onEvent(@NonNull Event event) {
     events.add(Assert.isNotNull(event));
-    for (Listener listener : listeners) {
+    for (Listener listener : new ArrayList<>(listeners)) {
       listener.onEvent(this, event);
     }
   }
diff --git a/java/com/android/dialer/simulator/impl/SimulatorConnectionService.java b/java/com/android/dialer/simulator/impl/SimulatorConnectionService.java
index 25d4a72..465890c 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorConnectionService.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorConnectionService.java
@@ -109,6 +109,19 @@
     return connection;
   }
 
+  @Override
+  public void onConference(Connection connection1, Connection connection2) {
+    LogUtil.i(
+        "SimulatorConnectionService.onConference",
+        "connection1: "
+            + SimulatorSimCallManager.getConnectionTag(connection1)
+            + ", connection2: "
+            + SimulatorSimCallManager.getConnectionTag(connection2));
+    for (Listener listener : listeners) {
+      listener.onConference((SimulatorConnection) connection1, (SimulatorConnection) connection2);
+    }
+  }
+
   private static Uri getPhoneNumber(ConnectionRequest request) {
     String phoneNumber = request.getExtras().getString(TelephonyManager.EXTRA_INCOMING_NUMBER);
     return Uri.fromParts(PhoneAccount.SCHEME_TEL, phoneNumber, null);
@@ -116,8 +129,11 @@
 
   /** Callback used to notify listeners when a new connection has been added. */
   public interface Listener {
-    void onNewOutgoingConnection(SimulatorConnection connection);
+    void onNewOutgoingConnection(@NonNull SimulatorConnection connection);
 
-    void onNewIncomingConnection(SimulatorConnection connection);
+    void onNewIncomingConnection(@NonNull SimulatorConnection connection);
+
+    void onConference(
+        @NonNull SimulatorConnection connection1, @NonNull SimulatorConnection connection2);
   }
 }
diff --git a/java/com/android/dialer/simulator/impl/SimulatorMissedCallCreator.java b/java/com/android/dialer/simulator/impl/SimulatorMissedCallCreator.java
index f85f466..6d4a262 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorMissedCallCreator.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorMissedCallCreator.java
@@ -62,6 +62,10 @@
         DISCONNECT_DELAY_MILLIS);
   }
 
+  @Override
+  public void onConference(
+      @NonNull SimulatorConnection connection1, @NonNull SimulatorConnection connection2) {}
+
   private void addNextIncomingCall(int callCount) {
     if (callCount <= 0) {
       LogUtil.i("SimulatorMissedCallCreator.addNextIncomingCall", "done adding calls");
diff --git a/java/com/android/dialer/simulator/impl/SimulatorSimCallManager.java b/java/com/android/dialer/simulator/impl/SimulatorSimCallManager.java
index 33eac51..00899fd 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorSimCallManager.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorSimCallManager.java
@@ -21,6 +21,7 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
+import android.telecom.Connection;
 import android.telecom.ConnectionRequest;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
@@ -47,6 +48,7 @@
   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";
+  private static final String EXTRA_CONNECTION_TAG = "connection_tag";
 
   static void register(@NonNull Context context) {
     LogUtil.enterBlock("SimulatorSimCallManager.register");
@@ -85,9 +87,7 @@
     register(context);
 
     extras = new Bundle(extras);
-    extras.putBoolean(EXTRA_IS_SIMULATOR_CONNECTION, true);
-    String connectionTag = createUniqueConnectionTag();
-    extras.putBoolean(connectionTag, true);
+    extras.putAll(createSimulatorConnectionExtras());
 
     Bundle outgoingCallExtras = new Bundle();
     outgoingCallExtras.putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, extras);
@@ -102,7 +102,7 @@
     } catch (SecurityException e) {
       throw Assert.createIllegalStateFailException("Unable to place call: " + e);
     }
-    return connectionTag;
+    return extras.getString(EXTRA_CONNECTION_TAG);
   }
 
   @NonNull
@@ -123,14 +123,12 @@
 
     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);
+    extras.putAll(createSimulatorConnectionExtras());
 
     TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
     telecomManager.addNewIncomingCall(
         isVideo ? getVideoProviderHandle(context) : getSystemPhoneAccountHandle(context), extras);
-    return connectionTag;
+    return extras.getString(EXTRA_CONNECTION_TAG);
   }
 
   @NonNull
@@ -167,7 +165,7 @@
   }
 
   @NonNull
-  private static PhoneAccountHandle getSystemPhoneAccountHandle(@NonNull Context context) {
+  public static PhoneAccountHandle getSystemPhoneAccountHandle(@NonNull Context context) {
     TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
     List<PhoneAccountHandle> handles;
     try {
@@ -190,10 +188,37 @@
   }
 
   @NonNull
+  public static String getConnectionTag(@NonNull Connection connection) {
+    String connectionTag = connection.getExtras().getString(EXTRA_CONNECTION_TAG);
+    return Assert.isNotNull(connectionTag);
+  }
+
+  @NonNull
+  public static SimulatorConnection findConnectionByTag(@NonNull String connectionTag) {
+    Assert.isNotNull(connectionTag);
+    for (Connection connection : SimulatorConnectionService.getInstance().getAllConnections()) {
+      if (connection.getExtras().getBoolean(connectionTag)) {
+        return (SimulatorConnection) connection;
+      }
+    }
+    throw Assert.createIllegalStateFailException();
+  }
+
+  @NonNull
   private static String createUniqueConnectionTag() {
     int callId = new Random().nextInt();
     return String.format("simulator_phone_call_%x", Math.abs(callId));
   }
 
+  @NonNull
+  static Bundle createSimulatorConnectionExtras() {
+    Bundle extras = new Bundle();
+    extras.putBoolean(EXTRA_IS_SIMULATOR_CONNECTION, true);
+    String connectionTag = createUniqueConnectionTag();
+    extras.putString(EXTRA_CONNECTION_TAG, connectionTag);
+    extras.putBoolean(connectionTag, true);
+    return extras;
+  }
+
   private SimulatorSimCallManager() {}
 }
diff --git a/java/com/android/dialer/simulator/impl/SimulatorSpamCallCreator.java b/java/com/android/dialer/simulator/impl/SimulatorSpamCallCreator.java
index 757658d..a843ec0 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorSpamCallCreator.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorSpamCallCreator.java
@@ -74,6 +74,10 @@
         DISCONNECT_DELAY_MILLIS);
   }
 
+  @Override
+  public void onConference(
+      @NonNull SimulatorConnection connection1, @NonNull SimulatorConnection connection2) {}
+
   private void addNextIncomingCall(int callCount) {
     if (callCount <= 0) {
       LogUtil.i("SimulatorSpamCallCreator.addNextIncomingCall", "done adding calls");
diff --git a/java/com/android/dialer/simulator/impl/SimulatorVideoCall.java b/java/com/android/dialer/simulator/impl/SimulatorVideoCall.java
index 3f00ab1..f7256a1 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorVideoCall.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorVideoCall.java
@@ -109,6 +109,10 @@
     }
   }
 
+  @Override
+  public void onConference(
+      @NonNull SimulatorConnection connection1, @NonNull SimulatorConnection connection2) {}
+
   private boolean isVideoAccountEnabled() {
     SimulatorSimCallManager.register(context);
     return context
@@ -150,15 +154,12 @@
       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();
+        LogUtil.i("SimulatorVideoCall.onEvent", "unexpected event: " + event.type);
+        break;
     }
   }
 }
diff --git a/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java b/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java
index 8eefb48..f478d55 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java
@@ -25,6 +25,7 @@
 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;
 import com.android.dialer.simulator.Simulator.Event;
 
 /** Entry point in the simulator to create voice calls. */
@@ -37,7 +38,10 @@
     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));
+        .addItem("Spam call", () -> new SimulatorVoiceCall(context).addNewIncomingCall(true))
+        .addItem(
+            "GSM conference",
+            () -> new SimulatorConferenceCreator(context, Simulator.CONFERENCE_TYPE_GSM).start(5));
   }
 
   private SimulatorVoiceCall(@NonNull Context context) {
@@ -62,21 +66,28 @@
 
   @Override
   public void onNewOutgoingConnection(@NonNull SimulatorConnection connection) {
-    if (connection.getExtras().getBoolean(connectionTag)) {
+    if (isMyConnection(connection)) {
       LogUtil.i("SimulatorVoiceCall.onNewOutgoingConnection", "connection created");
       handleNewConnection(connection);
-      connection.setActive();
+
+      // 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)) {
+    if (isMyConnection(connection)) {
       LogUtil.i("SimulatorVoiceCall.onNewIncomingConnection", "connection created");
       handleNewConnection(connection);
     }
   }
 
+  @Override
+  public void onConference(
+      @NonNull SimulatorConnection connection1, @NonNull SimulatorConnection connection2) {}
+
   private void handleNewConnection(@NonNull SimulatorConnection connection) {
     connection.addListener(this);
     connection.setConnectionCapabilities(
@@ -85,6 +96,10 @@
             | Connection.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL);
   }
 
+  private boolean isMyConnection(@NonNull Connection connection) {
+    return connection.getExtras().getBoolean(connectionTag);
+  }
+
   @Override
   public void onEvent(@NonNull SimulatorConnection connection, @NonNull Event event) {
     switch (event.type) {
@@ -105,15 +120,12 @@
       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();
+        LogUtil.i("SimulatorVoiceCall.onEvent", "unexpected event: " + event.type);
+        break;
     }
   }
 }