Refactor simulator menu and add portal package for adding simulator service later.

Bug: 79488174
Test: build dialer.
PiperOrigin-RevId: 196565757
Change-Id: Ic87c2640d856e25f3d7d476edc4fa36588351ece
diff --git a/java/com/android/dialer/simulator/impl/SimulatorConferenceCreator.java b/java/com/android/dialer/simulator/impl/SimulatorConferenceCreator.java
index 81a3d30..8e794f0 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorConferenceCreator.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorConferenceCreator.java
@@ -32,7 +32,7 @@
 import java.util.Locale;
 
 /** Creates a conference with a given number of participants. */
-final class SimulatorConferenceCreator
+public final class SimulatorConferenceCreator
     implements SimulatorConnectionService.Listener,
         SimulatorConnection.Listener,
         SimulatorConference.Listener {
@@ -53,7 +53,12 @@
     simulatorConnectionsBank = SimulatorComponent.get(context).getSimulatorConnectionsBank();
   }
 
-  void start(int callCount) {
+  /**
+   * Starts to create certain number of calls to form a conference call.
+   *
+   * @param callCount the number of calls in conference to create.
+   */
+  public void start(int callCount) {
     onNewIncomingConnectionEnabled = true;
     SimulatorConnectionService.addListener(this);
     if (conferenceType == Simulator.CONFERENCE_TYPE_VOLTE) {
diff --git a/java/com/android/dialer/simulator/impl/SimulatorImpl.java b/java/com/android/dialer/simulator/impl/SimulatorImpl.java
index c8b8af9..44f8e11 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorImpl.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorImpl.java
@@ -22,6 +22,7 @@
 import com.android.dialer.buildtype.BuildType.Type;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.simulator.Simulator;
+import com.android.dialer.simulator.portal.SimulatorMainPortal;
 import javax.inject.Inject;
 
 /** The entry point for the simulator feature. */
@@ -39,7 +40,7 @@
 
   @Override
   public ActionProvider getActionProvider(AppCompatActivity activity) {
-    return SimulatorMainMenu.getActionProvider(activity);
+    return new SimulatorMainPortal(activity).getActionProvider();
   }
 
   @Override
diff --git a/java/com/android/dialer/simulator/impl/SimulatorMissedCallCreator.java b/java/com/android/dialer/simulator/impl/SimulatorMissedCallCreator.java
index b855615..52d7352 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorMissedCallCreator.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorMissedCallCreator.java
@@ -30,14 +30,14 @@
  * notifications instead of writing to the call log directly. This makes the simulator behave more
  * like the real application.
  */
-final class SimulatorMissedCallCreator implements SimulatorConnectionService.Listener {
+public final class SimulatorMissedCallCreator implements SimulatorConnectionService.Listener {
   private static final String EXTRA_CALL_COUNT = "call_count";
   private static final String EXTRA_IS_MISSED_CALL_CONNECTION = "is_missed_call_connection";
   private static final int DISCONNECT_DELAY_MILLIS = 1000;
 
   private final Context context;
 
-  SimulatorMissedCallCreator(@NonNull Context context) {
+  public SimulatorMissedCallCreator(@NonNull Context context) {
     this.context = Assert.isNotNull(context);
   }
 
diff --git a/java/com/android/dialer/simulator/impl/SimulatorNotifications.java b/java/com/android/dialer/simulator/impl/SimulatorNotifications.java
deleted file mode 100644
index bd17b7f..0000000
--- a/java/com/android/dialer/simulator/impl/SimulatorNotifications.java
+++ /dev/null
@@ -1,62 +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.ContentValues;
-import android.content.Context;
-import android.provider.VoicemailContract.Voicemails;
-import android.support.annotation.NonNull;
-import android.view.ActionProvider;
-import com.android.dialer.common.LogUtil;
-import com.android.dialer.databasepopulator.VoicemailPopulator;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-/** Implements the simulator submenu. */
-final class SimulatorNotifications {
-  private static final int NOTIFICATION_COUNT = 12;
-
-  static ActionProvider getActionProvider(@NonNull Context context) {
-    return new SimulatorSubMenu(context)
-        .addItem(
-            "Missed calls", () -> new SimulatorMissedCallCreator(context).start(NOTIFICATION_COUNT))
-        .addItem("Voicemails", () -> addVoicemailNotifications(context));
-  }
-
-  private static void addVoicemailNotifications(@NonNull Context context) {
-    LogUtil.enterBlock("SimulatorNotifications.addVoicemailNotifications");
-    List<ContentValues> voicemails = new ArrayList<>();
-    for (int i = NOTIFICATION_COUNT; i > 0; i--) {
-      VoicemailPopulator.Voicemail voicemail =
-          VoicemailPopulator.Voicemail.builder()
-              .setPhoneNumber(String.format("+%d", i))
-              .setTranscription(String.format("Short transcript %d", i))
-              .setDurationSeconds(60)
-              .setIsRead(false)
-              .setPhoneAccountComponentName("")
-              .setTimeMillis(System.currentTimeMillis() - TimeUnit.HOURS.toMillis(i))
-              .build();
-      voicemails.add(voicemail.getAsContentValues(context));
-    }
-    context
-        .getContentResolver()
-        .bulkInsert(
-            Voicemails.buildSourceUri(context.getPackageName()),
-            voicemails.toArray(new ContentValues[voicemails.size()]));
-  }
-}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorRttCall.java b/java/com/android/dialer/simulator/impl/SimulatorRttCall.java
index 352b9e4..afd8977 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorRttCall.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorRttCall.java
@@ -21,7 +21,6 @@
 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;
@@ -29,28 +28,21 @@
 import com.android.dialer.simulator.Simulator.Event;
 
 /** Entry point in the simulator to create voice calls. */
-final class SimulatorRttCall
+public final class SimulatorRttCall
     implements SimulatorConnectionService.Listener, SimulatorConnection.Listener {
 
   @NonNull private final Context context;
   @Nullable private String connectionTag;
   private RttChatBot rttChatBot;
 
-  static ActionProvider getActionProvider(@NonNull Context context) {
-    return new SimulatorSubMenu(context)
-        .addItem("Incoming call", () -> new SimulatorRttCall(context).addNewIncomingCall(false))
-        .addItem("Outgoing call", () -> new SimulatorRttCall(context).addNewOutgoingCall())
-        .addItem("Emergency call", () -> new SimulatorRttCall(context).addNewEmergencyCall());
-  }
-
-  private SimulatorRttCall(@NonNull Context context) {
+  public SimulatorRttCall(@NonNull Context context) {
     this.context = Assert.isNotNull(context);
     SimulatorConnectionService.addListener(this);
     SimulatorConnectionService.addListener(
         new SimulatorConferenceCreator(context, Simulator.CONFERENCE_TYPE_GSM));
   }
 
-  private void addNewIncomingCall(boolean isSpam) {
+  public void addNewIncomingCall(boolean isSpam) {
     String callerId =
         isSpam
             ? "+1-661-778-3020" /* Blacklisted custom spam number */
@@ -60,14 +52,14 @@
             context, callerId, SimulatorSimCallManager.CALL_TYPE_RTT);
   }
 
-  private void addNewOutgoingCall() {
+  public void addNewOutgoingCall() {
     String callerId = "+55-31-2128-6800"; // Brazil office.
     connectionTag =
         SimulatorSimCallManager.addNewOutgoingCall(
             context, callerId, SimulatorSimCallManager.CALL_TYPE_RTT);
   }
 
-  private void addNewEmergencyCall() {
+  public void addNewEmergencyCall() {
     String callerId = "911";
     connectionTag =
         SimulatorSimCallManager.addNewIncomingCall(
diff --git a/java/com/android/dialer/simulator/impl/SimulatorSimCallManager.java b/java/com/android/dialer/simulator/impl/SimulatorSimCallManager.java
index c56afb2..0296ace 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorSimCallManager.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorSimCallManager.java
@@ -64,7 +64,7 @@
   private static final String EXTRA_CONNECTION_TAG = "connection_tag";
   private static final String EXTRA_CONNECTION_CALL_TYPE = "connection_call_type";
 
-  static void register(@NonNull Context context) {
+  public static void register(@NonNull Context context) {
     LogUtil.enterBlock("SimulatorSimCallManager.register");
     Assert.isNotNull(context);
     StrictModeUtils.bypass(
@@ -75,7 +75,7 @@
         });
   }
 
-  static void unregister(@NonNull Context context) {
+  public static void unregister(@NonNull Context context) {
     LogUtil.enterBlock("SimulatorSimCallManager.unregister");
     Assert.isNotNull(context);
     StrictModeUtils.bypass(
diff --git a/java/com/android/dialer/simulator/impl/SimulatorSubMenu.java b/java/com/android/dialer/simulator/impl/SimulatorSubMenu.java
deleted file mode 100644
index 64a2e72..0000000
--- a/java/com/android/dialer/simulator/impl/SimulatorSubMenu.java
+++ /dev/null
@@ -1,100 +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.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/SimulatorMainMenu.java b/java/com/android/dialer/simulator/impl/SimulatorUtils.java
similarity index 61%
rename from java/com/android/dialer/simulator/impl/SimulatorMainMenu.java
rename to java/com/android/dialer/simulator/impl/SimulatorUtils.java
index ba2b558..9e46f5a 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorMainMenu.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorUtils.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,73 +16,35 @@
 
 package com.android.dialer.simulator.impl;
 
+import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Voicemails;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
-import android.support.v7.app.AppCompatActivity;
-import android.view.ActionProvider;
+import com.android.dialer.common.LogUtil;
 import com.android.dialer.common.concurrent.DialerExecutor.Worker;
 import com.android.dialer.common.concurrent.DialerExecutorComponent;
 import com.android.dialer.databasepopulator.BlockedBumberPopulator;
 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;
 import com.android.dialer.preferredsim.PreferredSimFallbackContract;
-import com.android.dialer.simulator.SimulatorComponent;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.TimeUnit;
 
-/** Implements the top level simulator menu. */
-final class SimulatorMainMenu {
+/** Contains utilities used often in test workflow. */
+public class SimulatorUtils {
 
-  static ActionProvider getActionProvider(@NonNull AppCompatActivity activity) {
-    SimulatorSubMenu simulatorSubMenu = new SimulatorSubMenu(activity.getApplicationContext());
-    simulatorSubMenu
-        .addItem("Voice call", SimulatorVoiceCall.getActionProvider(activity))
-        .addItem("Rtt call", SimulatorRttCall.getActionProvider(activity.getApplicationContext()))
-        .addItem(
-            "IMS video", SimulatorVideoCall.getActionProvider(activity.getApplicationContext()))
-        .addItem(
-            "Notifications",
-            SimulatorNotifications.getActionProvider(activity.getApplicationContext()))
-        .addItem("Populate database", () -> populateDatabase(activity.getApplicationContext()))
-        .addItem("Populate voicemail", () -> populateVoicemail(activity.getApplicationContext()))
-        .addItem(
-            "Fast populate database", () -> fastPopulateDatabase(activity.getApplicationContext()))
-        .addItem(
-            "Fast populate voicemail database",
-            () -> populateVoicemailFast(activity.getApplicationContext()))
-        .addItem("Clean database", () -> cleanDatabase(activity.getApplicationContext()))
-        .addItem("clear preferred SIM", () -> clearPreferredSim(activity.getApplicationContext()))
-        .addItem("Sync voicemail", () -> syncVoicemail(activity.getApplicationContext()))
-        .addItem("Share persistent log", () -> sharePersistentLog(activity.getApplicationContext()))
-        .addItem(
-            "Enriched call simulator",
-            () ->
-                activity.startActivity(
-                    EnrichedCallSimulatorActivity.newIntent(activity.getApplicationContext())))
-        .addItem(
-            "Enable simulator mode",
-            () -> {
-              SimulatorComponent.get(activity.getApplicationContext())
-                  .getSimulator()
-                  .enableSimulatorMode();
-              SimulatorSimCallManager.register(activity.getApplicationContext());
-            })
-        .addItem(
-            "Disable simulator mode",
-            () -> {
-              SimulatorComponent.get(activity.getApplicationContext())
-                  .getSimulator()
-                  .disableSimulatorMode();
-              SimulatorSimCallManager.unregister(activity.getApplicationContext());
-            });
-    return simulatorSubMenu;
-  }
+  public static final int NOTIFICATION_COUNT_FEW = 1;
+  public static final int NOTIFICATION_COUNT = 12;
 
-  private static void populateDatabase(@NonNull Context context) {
+  /** Populates contacts database with predefined contacts entries. */
+  public static void populateDatabase(@NonNull Context context) {
     DialerExecutorComponent.get(context)
         .dialerExecutorFactory()
         .createNonUiTaskBuilder(new PopulateDatabaseWorker())
@@ -90,7 +52,8 @@
         .executeSerial(new PopulateDatabaseWorkerInput(context, false));
   }
 
-  private static void populateVoicemail(@NonNull Context context) {
+  /** Populates voicemail database with predefined voicemail entries. */
+  public static void populateVoicemail(@NonNull Context context) {
     DialerExecutorComponent.get(context)
         .dialerExecutorFactory()
         .createNonUiTaskBuilder(new PopulateVoicemailWorker())
@@ -98,7 +61,8 @@
         .executeSerial(new PopulateDatabaseWorkerInput(context, false));
   }
 
-  private static void populateVoicemailFast(@NonNull Context context) {
+  /** Populates voicemail database with only few predefined voicemail entries. */
+  public static void populateVoicemailFast(@NonNull Context context) {
     DialerExecutorComponent.get(context)
         .dialerExecutorFactory()
         .createNonUiTaskBuilder(new PopulateVoicemailWorker())
@@ -106,17 +70,8 @@
         .executeSerial(new PopulateDatabaseWorkerInput(context, true));
   }
 
-  private static class PopulateVoicemailWorker
-      implements Worker<PopulateDatabaseWorkerInput, Void> {
-    @Nullable
-    @Override
-    public Void doInBackground(PopulateDatabaseWorkerInput input) {
-      VoicemailPopulator.populateVoicemail(input.context, input.fastMode);
-      return null;
-    }
-  }
-
-  private static void fastPopulateDatabase(@NonNull Context context) {
+  /** Populates contacts database with only few predefined contacts entries. */
+  public static void fastPopulateDatabase(@NonNull Context context) {
     DialerExecutorComponent.get(context)
         .dialerExecutorFactory()
         .createNonUiTaskBuilder(new PopulateDatabaseWorker())
@@ -124,7 +79,8 @@
         .executeSerial(new PopulateDatabaseWorkerInput(context, true));
   }
 
-  private static void cleanDatabase(@NonNull Context context) {
+  /** Clean contacts database. */
+  public static void cleanDatabase(@NonNull Context context) {
     DialerExecutorComponent.get(context)
         .dialerExecutorFactory()
         .createNonUiTaskBuilder(new CleanDatabaseWorker())
@@ -132,7 +88,8 @@
         .executeSerial(context);
   }
 
-  private static void clearPreferredSim(Context context) {
+  /** Clear preference over sim. */
+  public static void clearPreferredSim(Context context) {
     DialerExecutorComponent.get(context)
         .dialerExecutorFactory()
         .createNonUiTaskBuilder(new ClearPreferredSimWorker())
@@ -140,12 +97,13 @@
         .executeSerial(context);
   }
 
-  private static void syncVoicemail(@NonNull Context context) {
+  /** Sync voicemail by sending intents to system. */
+  public static void syncVoicemail(@NonNull Context context) {
     Intent intent = new Intent(VoicemailContract.ACTION_SYNC_VOICEMAIL);
     context.sendBroadcast(intent);
   }
 
-  private static void sharePersistentLog(@NonNull Context context) {
+  public static void sharePersistentLog(@NonNull Context context) {
     DialerExecutorComponent.get(context)
         .dialerExecutorFactory()
         .createNonUiTaskBuilder(new ShareLogWorker())
@@ -162,7 +120,37 @@
         .executeSerial(null);
   }
 
-  private SimulatorMainMenu() {}
+  public static void addVoicemailNotifications(@NonNull Context context, int notificationNum) {
+    LogUtil.enterBlock("SimulatorNotifications.addVoicemailNotifications");
+    List<ContentValues> voicemails = new ArrayList<>();
+    for (int i = notificationNum; i > 0; i--) {
+      VoicemailPopulator.Voicemail voicemail =
+          VoicemailPopulator.Voicemail.builder()
+              .setPhoneNumber(String.format(Locale.ENGLISH, "+%d", i))
+              .setTranscription(String.format(Locale.ENGLISH, "Short transcript %d", i))
+              .setDurationSeconds(60)
+              .setIsRead(false)
+              .setPhoneAccountComponentName("")
+              .setTimeMillis(System.currentTimeMillis() - TimeUnit.HOURS.toMillis(i))
+              .build();
+      voicemails.add(voicemail.getAsContentValues(context));
+    }
+    context
+        .getContentResolver()
+        .bulkInsert(
+            Voicemails.buildSourceUri(context.getPackageName()),
+            voicemails.toArray(new ContentValues[voicemails.size()]));
+  }
+
+  private static class PopulateVoicemailWorker
+      implements Worker<PopulateDatabaseWorkerInput, Void> {
+    @Nullable
+    @Override
+    public Void doInBackground(PopulateDatabaseWorkerInput input) {
+      VoicemailPopulator.populateVoicemail(input.context, input.fastMode);
+      return null;
+    }
+  }
 
   private static class PopulateDatabaseWorker implements Worker<PopulateDatabaseWorkerInput, Void> {
     @Nullable
diff --git a/java/com/android/dialer/simulator/impl/SimulatorVideoCall.java b/java/com/android/dialer/simulator/impl/SimulatorVideoCall.java
index 0bb56f1..c7c92e7 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorVideoCall.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorVideoCall.java
@@ -23,8 +23,6 @@
 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;
@@ -32,36 +30,14 @@
 import com.android.dialer.simulator.Simulator.Event;
 
 /** Entry point in the simulator to create video calls. */
-final class SimulatorVideoCall
+public 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) {
+  public SimulatorVideoCall(@NonNull Context context, int initialVideoState) {
     this.context = Assert.isNotNull(context);
     this.initialVideoCapability =
         Connection.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL
@@ -70,7 +46,7 @@
     SimulatorConnectionService.addListener(this);
   }
 
-  private void addNewIncomingCall() {
+  public void addNewIncomingCall() {
     if (!isVideoAccountEnabled()) {
       showVideoAccountSettings();
       return;
@@ -81,7 +57,7 @@
             context, callerId, SimulatorSimCallManager.CALL_TYPE_VIDEO);
   }
 
-  private void addNewOutgoingCall() {
+  public void addNewOutgoingCall() {
     if (!isVideoAccountEnabled()) {
       showVideoAccountSettings();
       return;
diff --git a/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java b/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java
index e59cddd..a9f332e 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java
@@ -24,7 +24,6 @@
 import android.telecom.Connection;
 import android.telecom.Connection.RttModifyStatus;
 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.DialerExecutorComponent;
@@ -37,58 +36,13 @@
 import com.android.dialer.simulator.SimulatorEnrichedCall;
 
 /** Entry point in the simulator to create voice calls. */
-final class SimulatorVoiceCall
+public final class SimulatorVoiceCall
     implements SimulatorConnectionService.Listener, SimulatorConnection.Listener {
   @NonNull private final Context context;
   @Nullable private String connectionTag;
   private final SimulatorEnrichedCall simulatorEnrichedCall;
 
-  static ActionProvider getActionProvider(@NonNull AppCompatActivity activity) {
-    return new SimulatorSubMenu(activity.getApplicationContext())
-        .addItem(
-            "Incoming call",
-            () -> new SimulatorVoiceCall(activity.getApplicationContext()).addNewIncomingCall())
-        .addItem(
-            "Outgoing call",
-            () -> new SimulatorVoiceCall(activity.getApplicationContext()).addNewOutgoingCall())
-        .addItem(
-            "Customized incoming call",
-            () ->
-                new SimulatorVoiceCall(activity.getApplicationContext())
-                    .addNewIncomingCall(activity))
-        .addItem(
-            "Customized outgoing call",
-            () ->
-                new SimulatorVoiceCall(activity.getApplicationContext())
-                    .addNewOutgoingCall(activity))
-        .addItem(
-            "Incoming enriched call",
-            () -> new SimulatorVoiceCall(activity.getApplicationContext()).incomingEnrichedCall())
-        .addItem(
-            "Outgoing enriched call",
-            () -> new SimulatorVoiceCall(activity.getApplicationContext()).outgoingEnrichedCall())
-        .addItem(
-            "Spam incoming call",
-            () -> new SimulatorVoiceCall(activity.getApplicationContext()).addSpamIncomingCall())
-        .addItem(
-            "Emergency call back",
-            () ->
-                new SimulatorVoiceCall(activity.getApplicationContext()).addNewEmergencyCallBack())
-        .addItem(
-            "GSM conference",
-            () ->
-                new SimulatorConferenceCreator(
-                        activity.getApplicationContext(), Simulator.CONFERENCE_TYPE_GSM)
-                    .start(5))
-        .addItem(
-            "VoLTE conference",
-            () ->
-                new SimulatorConferenceCreator(
-                        activity.getApplicationContext(), Simulator.CONFERENCE_TYPE_VOLTE)
-                    .start(5));
-  }
-
-  private SimulatorVoiceCall(@NonNull Context context) {
+  public SimulatorVoiceCall(@NonNull Context context) {
     this.context = Assert.isNotNull(context);
     simulatorEnrichedCall = SimulatorComponent.get(context).getSimulatorEnrichedCall();
     SimulatorConnectionService.addListener(this);
@@ -96,7 +50,7 @@
         new SimulatorConferenceCreator(context, Simulator.CONFERENCE_TYPE_GSM));
   }
 
-  private void incomingEnrichedCall() {
+  public void incomingEnrichedCall() {
     simulatorEnrichedCall
         .setupIncomingEnrichedCall(Simulator.ENRICHED_CALL_INCOMING_NUMBER)
         .addListener(
@@ -113,7 +67,7 @@
             DialerExecutorComponent.get(context).uiExecutor());
   }
 
-  private void outgoingEnrichedCall() {
+  public void outgoingEnrichedCall() {
     getEnrichedCallManager().registerStateChangedListener(simulatorEnrichedCall);
     simulatorEnrichedCall
         .setupOutgoingEnrichedCall(Simulator.ENRICHED_CALL_OUTGOING_NUMBER)
@@ -131,14 +85,14 @@
             DialerExecutorComponent.get(context).uiExecutor());
   }
 
-  private void addNewIncomingCall() {
+  public void addNewIncomingCall() {
     String callerId = "+44 (0) 20 7031 3000" /* Google London office */;
     connectionTag =
         SimulatorSimCallManager.addNewIncomingCall(
             context, callerId, SimulatorSimCallManager.CALL_TYPE_VOICE);
   }
 
-  private void addNewIncomingCall(AppCompatActivity activity) {
+  public void addNewIncomingCall(AppCompatActivity activity) {
     SimulatorDialogFragment.newInstance(
             (callerId, callerIdPresentation) -> {
               Bundle extras = new Bundle();
@@ -150,14 +104,14 @@
         .show(activity.getSupportFragmentManager(), "SimulatorDialog");
   }
 
-  private void addNewOutgoingCall() {
+  public void addNewOutgoingCall() {
     String callerId = "+55-31-2128-6800"; // Brazil office.
     connectionTag =
         SimulatorSimCallManager.addNewOutgoingCall(
             context, callerId, SimulatorSimCallManager.CALL_TYPE_VOICE);
   }
 
-  private void addNewOutgoingCall(AppCompatActivity activity) {
+  public void addNewOutgoingCall(AppCompatActivity activity) {
     SimulatorDialogFragment.newInstance(
             (callerId, callerIdPresentation) -> {
               Bundle extras = new Bundle();
@@ -169,14 +123,14 @@
         .show(activity.getSupportFragmentManager(), "SimulatorDialog");
   }
 
-  private void addSpamIncomingCall() {
+  public void addSpamIncomingCall() {
     String callerId = "+1-661-778-3020"; /* Blacklisted custom spam number */
     connectionTag =
         SimulatorSimCallManager.addNewIncomingCall(
             context, callerId, SimulatorSimCallManager.CALL_TYPE_VOICE);
   }
 
-  private void addNewEmergencyCallBack() {
+  public void addNewEmergencyCallBack() {
     String callerId = "911";
     connectionTag =
         SimulatorSimCallManager.addNewIncomingCall(
diff --git a/java/com/android/dialer/simulator/portal/SimulatorMainPortal.java b/java/com/android/dialer/simulator/portal/SimulatorMainPortal.java
new file mode 100644
index 0000000..fdec2a5
--- /dev/null
+++ b/java/com/android/dialer/simulator/portal/SimulatorMainPortal.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.simulator.portal;
+
+import android.content.Context;
+import android.support.v7.app.AppCompatActivity;
+import android.telecom.VideoProfile;
+import android.view.ActionProvider;
+import com.android.dialer.enrichedcall.simulator.EnrichedCallSimulatorActivity;
+import com.android.dialer.simulator.Simulator;
+import com.android.dialer.simulator.SimulatorComponent;
+import com.android.dialer.simulator.impl.SimulatorConferenceCreator;
+import com.android.dialer.simulator.impl.SimulatorMissedCallCreator;
+import com.android.dialer.simulator.impl.SimulatorRttCall;
+import com.android.dialer.simulator.impl.SimulatorSimCallManager;
+import com.android.dialer.simulator.impl.SimulatorUtils;
+import com.android.dialer.simulator.impl.SimulatorVideoCall;
+import com.android.dialer.simulator.impl.SimulatorVoiceCall;
+import com.google.common.collect.ImmutableMap;
+
+/** Implements the top level simulator menu. */
+public final class SimulatorMainPortal {
+
+  private final Context context;
+  private final AppCompatActivity activity;
+  private SimulatorPortalEntryGroup simulatorMainPortal;
+
+  public SimulatorMainPortal(AppCompatActivity activity) {
+    this.activity = activity;
+    this.context = activity.getApplicationContext();
+    buildMainPortal();
+  }
+
+  private void buildMainPortal() {
+    this.simulatorMainPortal =
+        SimulatorPortalEntryGroup.builder()
+            .setMethods(
+                ImmutableMap.<String, Runnable>builder()
+                    .put("Populate database", () -> SimulatorUtils.populateDatabase(context))
+                    .put("Populate voicemail", () -> SimulatorUtils.populateVoicemail(context))
+                    .put(
+                        "Fast Populate database",
+                        () -> SimulatorUtils.fastPopulateDatabase(context))
+                    .put(
+                        "Fast populate voicemail database",
+                        () -> SimulatorUtils.populateVoicemailFast(context))
+                    .put("Clean database", () -> SimulatorUtils.cleanDatabase(context))
+                    .put("clear preferred SIM", () -> SimulatorUtils.clearPreferredSim(context))
+                    .put("Sync voicemail", () -> SimulatorUtils.syncVoicemail(context))
+                    .put("Share persistent log", () -> SimulatorUtils.sharePersistentLog(context))
+                    .put(
+                        "Enriched call simulator",
+                        () ->
+                            context.startActivity(EnrichedCallSimulatorActivity.newIntent(context)))
+                    .put(
+                        "Enable simulator mode",
+                        () -> {
+                          SimulatorComponent.get(context).getSimulator().enableSimulatorMode();
+                          SimulatorSimCallManager.register(context);
+                        })
+                    .put(
+                        "Disable simulator mode",
+                        () -> {
+                          SimulatorComponent.get(context).getSimulator().disableSimulatorMode();
+                          SimulatorSimCallManager.unregister(context);
+                        })
+                    .build())
+            .setSubPortals(
+                ImmutableMap.of(
+                    "VoiceCall",
+                    buildSimulatorVoiceCallPortal(),
+                    "VideoCall",
+                    buildSimulatorVideoCallPortal(),
+                    "RttCall",
+                    buildSimulatorRttCallPortal(),
+                    "Notifications",
+                    buildSimulatorNotificationsPortal()))
+            .build();
+  }
+
+  public SimulatorPortalEntryGroup buildSimulatorVoiceCallPortal() {
+    return SimulatorPortalEntryGroup.builder()
+        .setMethods(
+            ImmutableMap.<String, Runnable>builder()
+                .put("Incoming call", () -> new SimulatorVoiceCall(context).addNewIncomingCall())
+                .put("Outgoing call", () -> new SimulatorVoiceCall(context).addNewOutgoingCall())
+                .put(
+                    "Customized incoming call",
+                    () -> new SimulatorVoiceCall(context).addNewIncomingCall(activity))
+                .put(
+                    "Customized outgoing call",
+                    () -> new SimulatorVoiceCall(context).addNewOutgoingCall(activity))
+                .put(
+                    "Incoming enriched call",
+                    () -> new SimulatorVoiceCall(context).incomingEnrichedCall())
+                .put(
+                    "Outgoing enriched call",
+                    () -> new SimulatorVoiceCall(context).outgoingEnrichedCall())
+                .put(
+                    "Spam incoming call",
+                    () -> new SimulatorVoiceCall(context).addSpamIncomingCall())
+                .put(
+                    "Emergency call back",
+                    () -> new SimulatorVoiceCall(context).addNewEmergencyCallBack())
+                .put(
+                    "GSM conference",
+                    () ->
+                        new SimulatorConferenceCreator(context, Simulator.CONFERENCE_TYPE_GSM)
+                            .start(5))
+                .put(
+                    "VoLTE conference",
+                    () ->
+                        new SimulatorConferenceCreator(context, Simulator.CONFERENCE_TYPE_VOLTE)
+                            .start(5))
+                .build())
+        .build();
+  }
+
+  private SimulatorPortalEntryGroup buildSimulatorVideoCallPortal() {
+    return SimulatorPortalEntryGroup.builder()
+        .setMethods(
+            ImmutableMap.<String, Runnable>builder()
+                .put(
+                    "Incoming one way",
+                    () ->
+                        new SimulatorVideoCall(context, VideoProfile.STATE_RX_ENABLED)
+                            .addNewIncomingCall())
+                .put(
+                    "Incoming two way",
+                    () ->
+                        new SimulatorVideoCall(context, VideoProfile.STATE_BIDIRECTIONAL)
+                            .addNewIncomingCall())
+                .put(
+                    "Outgoing one way",
+                    () ->
+                        new SimulatorVideoCall(context, VideoProfile.STATE_TX_ENABLED)
+                            .addNewOutgoingCall())
+                .put(
+                    "Outgoing two way",
+                    () ->
+                        new SimulatorVideoCall(context, VideoProfile.STATE_BIDIRECTIONAL)
+                            .addNewOutgoingCall())
+                .build())
+        .build();
+  }
+
+  private SimulatorPortalEntryGroup buildSimulatorRttCallPortal() {
+    return SimulatorPortalEntryGroup.builder()
+        .setMethods(
+            ImmutableMap.<String, Runnable>builder()
+                .put("Incoming call", () -> new SimulatorRttCall(context).addNewIncomingCall(false))
+                .put("Outgoing call", () -> new SimulatorRttCall(context).addNewOutgoingCall())
+                .put("Emergency call", () -> new SimulatorRttCall(context).addNewEmergencyCall())
+                .build())
+        .build();
+  }
+
+  private SimulatorPortalEntryGroup buildSimulatorNotificationsPortal() {
+    return SimulatorPortalEntryGroup.builder()
+        .setMethods(
+            ImmutableMap.<String, Runnable>builder()
+                .put(
+                    "Missed calls",
+                    () ->
+                        new SimulatorMissedCallCreator(context)
+                            .start(SimulatorUtils.NOTIFICATION_COUNT))
+                .put(
+                    "Missed calls (few)",
+                    () ->
+                        new SimulatorMissedCallCreator(context)
+                            .start(SimulatorUtils.NOTIFICATION_COUNT_FEW))
+                .put(
+                    "Voicemails",
+                    () ->
+                        SimulatorUtils.addVoicemailNotifications(
+                            context, SimulatorUtils.NOTIFICATION_COUNT))
+                .build())
+        .build();
+  }
+
+  public ActionProvider getActionProvider() {
+    return new SimulatorMenu(context, simulatorMainPortal);
+  }
+}
diff --git a/java/com/android/dialer/simulator/portal/SimulatorMenu.java b/java/com/android/dialer/simulator/portal/SimulatorMenu.java
new file mode 100644
index 0000000..01fd4aa
--- /dev/null
+++ b/java/com/android/dialer/simulator/portal/SimulatorMenu.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.simulator.portal;
+
+import android.content.Context;
+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 java.util.Map.Entry;
+
+/** Makes option menu for simulator. */
+public final class SimulatorMenu extends ActionProvider {
+
+  SimulatorPortalEntryGroup portal;
+
+  Context context;
+
+  public SimulatorMenu(@NonNull Context context, SimulatorPortalEntryGroup portal) {
+    super(Assert.isNotNull(context));
+    this.context = context;
+    this.portal = portal;
+  }
+
+  @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 (Entry<String, SimulatorPortalEntryGroup> subPortal : portal.subPortals().entrySet()) {
+      subMenu
+          .add(subPortal.getKey())
+          .setActionProvider(new SimulatorMenu(context, subPortal.getValue()));
+    }
+    for (Entry<String, Runnable> method : portal.methods().entrySet()) {
+      subMenu
+          .add(method.getKey())
+          .setOnMenuItemClickListener(
+              (i) -> {
+                method.getValue().run();
+                return true;
+              });
+    }
+  }
+}
diff --git a/java/com/android/dialer/simulator/portal/SimulatorPortalEntryGroup.java b/java/com/android/dialer/simulator/portal/SimulatorPortalEntryGroup.java
new file mode 100644
index 0000000..30a248c
--- /dev/null
+++ b/java/com/android/dialer/simulator/portal/SimulatorPortalEntryGroup.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.simulator.portal;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableMap;
+import java.util.Collections;
+import java.util.Map;
+
+/** Represents a portal that receives requests from either UI or IPC. */
+@AutoValue
+public abstract class SimulatorPortalEntryGroup {
+  abstract ImmutableMap<String, Runnable> methods();
+
+  abstract ImmutableMap<String, SimulatorPortalEntryGroup> subPortals();
+
+  static Builder builder() {
+    return new AutoValue_SimulatorPortalEntryGroup.Builder()
+        .setMethods(Collections.emptyMap())
+        .setSubPortals(Collections.emptyMap());
+  }
+
+  @AutoValue.Builder
+  abstract static class Builder {
+    abstract Builder setMethods(Map<String, Runnable> value);
+
+    abstract Builder setSubPortals(Map<String, SimulatorPortalEntryGroup> value);
+
+    abstract SimulatorPortalEntryGroup build();
+  }
+}