Base Implementation of SipTransport

Implements create/destroy procedures for app/ImsService. This is the
first in a series of CLs to implement SipTransport in telephony.

- SipTransportController: manages the SipDelegate connections between IMS app and
ImsService for a single slot. SipDelegates are created and destroyed for the
subId associated with the slot.

- SipDelegateController: SipTransportController creates a new SipDelegateController
for each request from an IMS application. The SipDelegateController maintains
the SipDelegate created for the feature tags assigned to it from SipTransportController.

- SipDelegateBinderConnection: Connection to SipDelegate on ImsService side. Can be
brought up/down as the associated features change.

- DelegateStateTracker/MessageTransportStateTracker: wrapper around binder to IMS
application's implementation of SipDelegateConnection callbacks.

Test: atest TeleServiceTests
Change-Id: Ieabd13aa4c54fa069ef5fa1298b1749e4192e583
diff --git a/tests/src/com/android/TelephonyTestBase.java b/tests/src/com/android/TelephonyTestBase.java
index 132d893..502740d 100644
--- a/tests/src/com/android/TelephonyTestBase.java
+++ b/tests/src/com/android/TelephonyTestBase.java
@@ -27,6 +27,7 @@
 import org.mockito.MockitoAnnotations;
 
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -58,6 +59,23 @@
         PhoneConfigurationManager.unregisterAllMultiSimConfigChangeRegistrants();
     }
 
+    protected final boolean waitForExecutorAction(Executor executor, long timeoutMillis) {
+        final CountDownLatch lock = new CountDownLatch(1);
+        Log.i("BRAD", "waitForExecutorAction");
+        executor.execute(() -> {
+            Log.i("BRAD", "countdown");
+            lock.countDown();
+        });
+        while (lock.getCount() > 0) {
+            try {
+                return lock.await(timeoutMillis, TimeUnit.MILLISECONDS);
+            } catch (InterruptedException e) {
+                // do nothing
+            }
+        }
+        return true;
+    }
+
     protected final void waitForHandlerAction(Handler h, long timeoutMillis) {
         final CountDownLatch lock = new CountDownLatch(1);
         h.post(lock::countDown);
diff --git a/tests/src/com/android/services/telephony/rcs/DelegateStateTrackerTest.java b/tests/src/com/android/services/telephony/rcs/DelegateStateTrackerTest.java
new file mode 100644
index 0000000..4d40702
--- /dev/null
+++ b/tests/src/com/android/services/telephony/rcs/DelegateStateTrackerTest.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2020 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.services.telephony.rcs;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.telephony.ims.DelegateRegistrationState;
+import android.telephony.ims.FeatureTagState;
+import android.telephony.ims.SipDelegateImsConfiguration;
+import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.aidl.ISipDelegate;
+import android.telephony.ims.aidl.ISipDelegateConnectionStateCallback;
+import android.util.ArraySet;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.TelephonyTestBase;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+public class DelegateStateTrackerTest extends TelephonyTestBase {
+    private static final int TEST_SUB_ID = 1;
+
+    @Mock private ISipDelegate mSipDelegate;
+    @Mock private ISipDelegateConnectionStateCallback mAppCallback;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    /**
+     * When an underlying SipDelegate is created, the app should only receive one onCreated callback
+     * independent of how many times sipDelegateConnected is called. Once created, registration
+     * and IMS configuration events should propagate up to the app as well.
+     */
+    @SmallTest
+    @Test
+    public void testDelegateCreated() throws Exception {
+        DelegateStateTracker stateTracker = new DelegateStateTracker(TEST_SUB_ID, mAppCallback,
+                mSipDelegate);
+        Set<FeatureTagState> deniedTags = getMmTelDeniedTag();
+        stateTracker.sipDelegateConnected(deniedTags);
+        // Calling connected multiple times should not generate multiple onCreated events.
+        stateTracker.sipDelegateConnected(deniedTags);
+        verify(mAppCallback).onCreated(mSipDelegate);
+
+        // Ensure status updates are sent to app as expected.
+        DelegateRegistrationState regState = new DelegateRegistrationState.Builder()
+                .addRegisteredFeatureTag(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG)
+                .build();
+        SipDelegateImsConfiguration config = new SipDelegateImsConfiguration.Builder(1/*version*/)
+                .build();
+        stateTracker.onRegistrationStateChanged(regState);
+        stateTracker.onImsConfigurationChanged(config);
+        verify(mAppCallback).onFeatureTagStatusChanged(eq(regState),
+                eq(new ArrayList<>(deniedTags)));
+        verify(mAppCallback).onImsConfigurationChanged(config);
+
+        verify(mAppCallback, never()).onDestroyed(anyInt());
+    }
+
+    /**
+     * onDestroyed should be called when sipDelegateDestroyed is called.
+     */
+    @SmallTest
+    @Test
+    public void testDelegateDestroyed() throws Exception {
+        DelegateStateTracker stateTracker = new DelegateStateTracker(TEST_SUB_ID, mAppCallback,
+                mSipDelegate);
+        Set<FeatureTagState> deniedTags = getMmTelDeniedTag();
+        stateTracker.sipDelegateConnected(deniedTags);
+
+        stateTracker.sipDelegateDestroyed(
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+        verify(mAppCallback).onDestroyed(
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+    }
+
+    /**
+     * When a SipDelegate is created and then an event occurs that will destroy->create a new
+     * SipDelegate underneath, we need to move the state of the features that are reporting
+     * registered to DEREGISTERING_REASON_FEATURE_TAGS_CHANGING so that the app can close dialogs on
+     * it. Once the new underlying SipDelegate is created, we must verify that the new registration
+     * is propagated up without any overrides.
+     */
+    @SmallTest
+    @Test
+    public void testDelegateChangingRegisteredTagsOverride() throws Exception {
+        DelegateStateTracker stateTracker = new DelegateStateTracker(TEST_SUB_ID, mAppCallback,
+                mSipDelegate);
+        Set<FeatureTagState> deniedTags = getMmTelDeniedTag();
+        stateTracker.sipDelegateConnected(deniedTags);
+        // SipDelegate created
+        verify(mAppCallback).onCreated(mSipDelegate);
+        DelegateRegistrationState regState = new DelegateRegistrationState.Builder()
+                .addRegisteredFeatureTag(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG)
+                .addDeregisteringFeatureTag(ImsSignallingUtils.FILE_TRANSFER_HTTP_TAG,
+                        DelegateRegistrationState.DEREGISTERING_REASON_PROVISIONING_CHANGE)
+                .addDeregisteredFeatureTag(ImsSignallingUtils.GROUP_CHAT_TAG,
+                        DelegateRegistrationState.DEREGISTERED_REASON_NOT_PROVISIONED)
+                .build();
+        stateTracker.onRegistrationStateChanged(regState);
+        // Simulate underlying SipDelegate switch
+        stateTracker.sipDelegateChanging(
+                DelegateRegistrationState.DEREGISTERING_REASON_FEATURE_TAGS_CHANGING);
+        // onFeatureTagStatusChanged should now be called with registered features overridden with
+        // DEREGISTERING_REASON_FEATURE_TAGS_CHANGING
+        DelegateRegistrationState overrideRegState = new DelegateRegistrationState.Builder()
+                .addDeregisteringFeatureTag(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG,
+                        DelegateRegistrationState.DEREGISTERING_REASON_FEATURE_TAGS_CHANGING)
+                // Already Deregistering/Deregistered tags should not be overridden.
+                .addDeregisteringFeatureTag(ImsSignallingUtils.FILE_TRANSFER_HTTP_TAG,
+                        DelegateRegistrationState.DEREGISTERING_REASON_PROVISIONING_CHANGE)
+                .addDeregisteredFeatureTag(ImsSignallingUtils.GROUP_CHAT_TAG,
+                        DelegateRegistrationState.DEREGISTERED_REASON_NOT_PROVISIONED)
+                .build();
+        // new underlying SipDelegate created
+        stateTracker.sipDelegateConnected(deniedTags);
+        stateTracker.onRegistrationStateChanged(regState);
+
+        // Verify registration state through the process:
+        ArgumentCaptor<DelegateRegistrationState> regCaptor =
+                ArgumentCaptor.forClass(DelegateRegistrationState.class);
+        verify(mAppCallback, times(3)).onFeatureTagStatusChanged(
+                regCaptor.capture(), eq(new ArrayList<>(deniedTags)));
+        List<DelegateRegistrationState> testStates = regCaptor.getAllValues();
+        // feature tags should first be registered
+        assertEquals(regState, testStates.get(0));
+        // registered feature tags should have moved to deregistering
+        assertEquals(overrideRegState, testStates.get(1));
+        // and then moved back to registered after underlying FT change done.
+        assertEquals(regState, testStates.get(2));
+
+        //onCreate should only have been called once and onDestroy should have never been called.
+        verify(mAppCallback).onCreated(mSipDelegate);
+        verify(mAppCallback, never()).onDestroyed(anyInt());
+    }
+
+    /**
+     * Test the case that when the underlying Denied tags change in the SipDelegate, the change is
+     * properly shown in the registration update event.
+     */
+    @SmallTest
+    @Test
+    public void testDelegateChangingDeniedTagsChanged() throws Exception {
+        DelegateStateTracker stateTracker = new DelegateStateTracker(TEST_SUB_ID, mAppCallback,
+                mSipDelegate);
+        Set<FeatureTagState> deniedTags = getMmTelDeniedTag();
+        stateTracker.sipDelegateConnected(deniedTags);
+        // SipDelegate created
+        verify(mAppCallback).onCreated(mSipDelegate);
+        DelegateRegistrationState regState = new DelegateRegistrationState.Builder()
+                .addRegisteredFeatureTag(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG)
+                .build();
+        stateTracker.onRegistrationStateChanged(regState);
+        // Simulate underlying SipDelegate switch
+        stateTracker.sipDelegateChanging(
+                DelegateRegistrationState.DEREGISTERING_REASON_FEATURE_TAGS_CHANGING);
+        // onFeatureTagStatusChanged should now be called with registered features overridden with
+        // DEREGISTERING_REASON_FEATURE_TAGS_CHANGING
+        DelegateRegistrationState overrideRegState = new DelegateRegistrationState.Builder()
+                .addDeregisteringFeatureTag(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG,
+                        DelegateRegistrationState.DEREGISTERING_REASON_FEATURE_TAGS_CHANGING)
+                .build();
+        // Verify registration state so far.
+        ArgumentCaptor<DelegateRegistrationState> regCaptor =
+                ArgumentCaptor.forClass(DelegateRegistrationState.class);
+        verify(mAppCallback, times(2)).onFeatureTagStatusChanged(
+                regCaptor.capture(), eq(new ArrayList<>(deniedTags)));
+        List<DelegateRegistrationState> testStates = regCaptor.getAllValues();
+        assertEquals(2, testStates.size());
+        // feature tags should first be registered
+        assertEquals(regState, testStates.get(0));
+        // registered feature tags should have moved to deregistering
+        assertEquals(overrideRegState, testStates.get(1));
+
+        // new underlying SipDelegate created, but SipDelegate denied one to one chat
+        deniedTags.add(new FeatureTagState(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG,
+                SipDelegateManager.DENIED_REASON_NOT_ALLOWED));
+        stateTracker.sipDelegateConnected(deniedTags);
+        DelegateRegistrationState fullyDeniedRegState = new DelegateRegistrationState.Builder()
+                .build();
+        // In this special case, it will be the SipDelegateConnectionBase that will trigger
+        // reg state change.
+        stateTracker.onRegistrationStateChanged(fullyDeniedRegState);
+        verify(mAppCallback).onFeatureTagStatusChanged(regCaptor.capture(),
+                eq(new ArrayList<>(deniedTags)));
+        // now all feature tags denied, so we should see only denied tags.
+        assertEquals(fullyDeniedRegState, regCaptor.getValue());
+
+        //onCreate should only have been called once and onDestroy should have never been called.
+        verify(mAppCallback).onCreated(mSipDelegate);
+        verify(mAppCallback, never()).onDestroyed(anyInt());
+    }
+
+    /**
+     * Test that when we move from changing tags state to the delegate being destroyed, we get the
+     * correct onDestroy event sent to the app.
+     */
+    @SmallTest
+    @Test
+    public void testDelegateChangingDeniedTagsChangingToDestroy() throws Exception {
+        DelegateStateTracker stateTracker = new DelegateStateTracker(TEST_SUB_ID, mAppCallback,
+                mSipDelegate);
+        Set<FeatureTagState> deniedTags = getMmTelDeniedTag();
+        stateTracker.sipDelegateConnected(deniedTags);
+        // SipDelegate created
+        verify(mAppCallback).onCreated(mSipDelegate);
+        DelegateRegistrationState regState = new DelegateRegistrationState.Builder()
+                .addRegisteredFeatureTag(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG)
+                .addDeregisteredFeatureTag(ImsSignallingUtils.GROUP_CHAT_TAG,
+                        DelegateRegistrationState.DEREGISTERED_REASON_NOT_PROVISIONED)
+                .build();
+        stateTracker.onRegistrationStateChanged(regState);
+        verify(mAppCallback).onFeatureTagStatusChanged(any(),
+                eq(new ArrayList<>(deniedTags)));
+        // Simulate underlying SipDelegate switch
+        stateTracker.sipDelegateChanging(
+                DelegateRegistrationState.DEREGISTERING_REASON_DESTROY_PENDING);
+        // Destroy
+        stateTracker.sipDelegateDestroyed(
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+
+        // onFeatureTagStatusChanged should now be called with registered features overridden with
+        // DEREGISTERING_REASON_DESTROY_PENDING
+        DelegateRegistrationState overrideRegState = new DelegateRegistrationState.Builder()
+                .addDeregisteringFeatureTag(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG,
+                        DelegateRegistrationState.DEREGISTERING_REASON_DESTROY_PENDING)
+                // Deregistered should stay the same.
+                .addDeregisteredFeatureTag(ImsSignallingUtils.GROUP_CHAT_TAG,
+                        DelegateRegistrationState.DEREGISTERED_REASON_NOT_PROVISIONED)
+                .build();
+        // Verify registration state through process:
+        ArgumentCaptor<DelegateRegistrationState> regCaptor =
+                ArgumentCaptor.forClass(DelegateRegistrationState.class);
+        verify(mAppCallback, times(2)).onFeatureTagStatusChanged(regCaptor.capture(),
+                eq(new ArrayList<>(deniedTags)));
+        List<DelegateRegistrationState> testStates = regCaptor.getAllValues();
+        assertEquals(2, testStates.size());
+        // feature tags should first be registered
+        assertEquals(regState, testStates.get(0));
+        // registered feature tags should have moved to deregistering
+        assertEquals(overrideRegState, testStates.get(1));
+        //onCreate/onDestroy should only be called once.
+        verify(mAppCallback).onCreated(mSipDelegate);
+        verify(mAppCallback).onDestroyed(
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+    }
+
+    private Set<FeatureTagState> getMmTelDeniedTag() {
+        Set<FeatureTagState> deniedTags = new ArraySet<>();
+        deniedTags.add(new FeatureTagState(ImsSignallingUtils.MMTEL_TAG,
+                SipDelegateManager.DENIED_REASON_NOT_ALLOWED));
+        return deniedTags;
+    }
+}
diff --git a/tests/src/com/android/services/telephony/rcs/ImsSignallingUtils.java b/tests/src/com/android/services/telephony/rcs/ImsSignallingUtils.java
new file mode 100644
index 0000000..d607f6d
--- /dev/null
+++ b/tests/src/com/android/services/telephony/rcs/ImsSignallingUtils.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2020 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.services.telephony.rcs;
+
+/**
+ * Various definitions and utilities related to IMS Signalling.
+ */
+public class ImsSignallingUtils {
+    public static final String MMTEL_TAG =
+            "+g.3gpp.iari-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.mmtel\"";
+    public static final String ONE_TO_ONE_CHAT_TAG =
+            "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gppservice.ims.icsi.oma.cpm.msg\"";
+    public static final String GROUP_CHAT_TAG =
+            "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gppservice.ims.icsi.oma.cpm.session\"";
+    public static final String FILE_TRANSFER_HTTP_TAG =
+            "+g.3gpp.iari-ref=\"urn%3Aurn-7%3A3gppapplication.ims.iari.rcs.fthttp\"";
+}
diff --git a/tests/src/com/android/services/telephony/rcs/MessageTransportStateTrackerTest.java b/tests/src/com/android/services/telephony/rcs/MessageTransportStateTrackerTest.java
new file mode 100644
index 0000000..7ba4252
--- /dev/null
+++ b/tests/src/com/android/services/telephony/rcs/MessageTransportStateTrackerTest.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2020 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.services.telephony.rcs;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.os.RemoteException;
+import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.SipMessage;
+import android.telephony.ims.aidl.ISipDelegate;
+import android.telephony.ims.aidl.ISipDelegateMessageCallback;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.TelephonyTestBase;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+@RunWith(AndroidJUnit4.class)
+public class MessageTransportStateTrackerTest extends TelephonyTestBase {
+    private static final int TEST_SUB_ID = 1;
+
+    private static final SipMessage TEST_MESSAGE = new SipMessage(
+            "INVITE sip:callee@ex.domain.com SIP/2.0",
+            "Via: SIP/2.0/UDP ex.place.com;branch=z9hG4bK776asdhds",
+            new byte[0]);
+
+    // Use for finer-grained control of when the Executor executes.
+    private static class PendingExecutor implements Executor {
+        private final ArrayList<Runnable> mPendingRunnables = new ArrayList<>();
+
+        @Override
+        public void execute(Runnable command) {
+            mPendingRunnables.add(command);
+        }
+
+        public void executePending() {
+            for (Runnable r : mPendingRunnables) {
+                r.run();
+            }
+            mPendingRunnables.clear();
+        }
+    }
+
+    @Mock private ISipDelegateMessageCallback mDelegateMessageCallback;
+    @Mock private ISipDelegate mISipDelegate;
+    @Mock private Consumer<Boolean> mMockCloseConsumer;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @SmallTest
+    @Test
+    public void testDelegateConnectionSendOutgoingMessage() throws Exception {
+        MessageTransportStateTracker tracker = new MessageTransportStateTracker(TEST_SUB_ID,
+                Runnable::run, mDelegateMessageCallback);
+
+        tracker.openTransport(mISipDelegate, Collections.emptySet());
+        tracker.getDelegateConnection().sendMessage(TEST_MESSAGE, 1 /*version*/);
+        verify(mISipDelegate).sendMessage(TEST_MESSAGE, 1 /*version*/);
+
+        doThrow(new RemoteException()).when(mISipDelegate).sendMessage(any(), anyInt());
+        tracker.getDelegateConnection().sendMessage(TEST_MESSAGE, 1 /*version*/);
+        verify(mDelegateMessageCallback).onMessageSendFailure(any(),
+                eq(SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_DEAD));
+
+        tracker.close(SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED);
+        tracker.getDelegateConnection().sendMessage(TEST_MESSAGE, 1 /*version*/);
+        verify(mDelegateMessageCallback).onMessageSendFailure(any(),
+                eq(SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED));
+    }
+
+    @SmallTest
+    @Test
+    public void testDelegateConnectionCloseGracefully() throws Exception {
+        PendingExecutor executor = new PendingExecutor();
+        MessageTransportStateTracker tracker = new MessageTransportStateTracker(TEST_SUB_ID,
+                executor, mDelegateMessageCallback);
+
+        tracker.openTransport(mISipDelegate, Collections.emptySet());
+        tracker.getDelegateConnection().sendMessage(TEST_MESSAGE, 1 /*version*/);
+        executor.executePending();
+        verify(mISipDelegate).sendMessage(TEST_MESSAGE, 1 /*version*/);
+        verify(mDelegateMessageCallback, never()).onMessageSendFailure(any(), anyInt());
+
+        // Use PendingExecutor a little weird here, we need to queue sendMessage first, even though
+        // closeGracefully will complete partly synchronously to test that the pending message will
+        // return MESSAGE_FAILURE_REASON_INTERNAL_DELEGATE_STATE_TRANSITION before the scheduled
+        // graceful close operation completes.
+        tracker.getDelegateConnection().sendMessage(TEST_MESSAGE, 1 /*version*/);
+        tracker.closeGracefully(
+                SipDelegateManager.MESSAGE_FAILURE_REASON_INTERNAL_DELEGATE_STATE_TRANSITION,
+                SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED,
+                mMockCloseConsumer);
+        verify(mMockCloseConsumer, never()).accept(any());
+        // resolve pending close operation
+        executor.executePending();
+        verify(mDelegateMessageCallback).onMessageSendFailure(any(),
+                eq(SipDelegateManager.MESSAGE_FAILURE_REASON_INTERNAL_DELEGATE_STATE_TRANSITION));
+        // Still should only report one call of sendMessage from before
+        verify(mISipDelegate).sendMessage(TEST_MESSAGE, 1 /*version*/);
+        verify(mMockCloseConsumer).accept(true);
+
+        // ensure that after close operation completes, we get the correct
+        // MESSAGE_FAILURE_REASON_DELEGATE_CLOSED message.
+        tracker.getDelegateConnection().sendMessage(TEST_MESSAGE, 1 /*version*/);
+        executor.executePending();
+        verify(mDelegateMessageCallback).onMessageSendFailure(any(),
+                eq(SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED));
+        // Still should only report one call of sendMessage from before
+        verify(mISipDelegate).sendMessage(TEST_MESSAGE, 1 /*version*/);
+    }
+
+    @SmallTest
+    @Test
+    public void testDelegateConnectionNotifyMessageReceived() throws Exception {
+        MessageTransportStateTracker tracker = new MessageTransportStateTracker(TEST_SUB_ID,
+                Runnable::run, mDelegateMessageCallback);
+        tracker.openTransport(mISipDelegate, Collections.emptySet());
+        tracker.getDelegateConnection().notifyMessageReceived("z9hG4bK776asdhds");
+        verify(mISipDelegate).notifyMessageReceived("z9hG4bK776asdhds");
+    }
+
+    @SmallTest
+    @Test
+    public void testDelegateConnectionNotifyMessageReceiveError() throws Exception {
+        MessageTransportStateTracker tracker = new MessageTransportStateTracker(TEST_SUB_ID,
+                Runnable::run, mDelegateMessageCallback);
+        tracker.openTransport(mISipDelegate, Collections.emptySet());
+        tracker.getDelegateConnection().notifyMessageReceiveError("z9hG4bK776asdhds",
+                SipDelegateManager.MESSAGE_FAILURE_REASON_NETWORK_NOT_AVAILABLE);
+        verify(mISipDelegate).notifyMessageReceiveError("z9hG4bK776asdhds",
+                SipDelegateManager.MESSAGE_FAILURE_REASON_NETWORK_NOT_AVAILABLE);
+    }
+
+    @SmallTest
+    @Test
+    public void testDelegateConnectionCloseDialog() throws Exception {
+        MessageTransportStateTracker tracker = new MessageTransportStateTracker(TEST_SUB_ID,
+                Runnable::run, mDelegateMessageCallback);
+        tracker.openTransport(mISipDelegate, Collections.emptySet());
+        tracker.getDelegateConnection().closeDialog("testCallId");
+        verify(mISipDelegate).closeDialog("testCallId");
+    }
+
+    @SmallTest
+    @Test
+    public void testDelegateOnMessageReceived() throws Exception {
+        MessageTransportStateTracker tracker = new MessageTransportStateTracker(TEST_SUB_ID,
+                Runnable::run, mDelegateMessageCallback);
+        tracker.openTransport(mISipDelegate, Collections.emptySet());
+
+        tracker.getMessageCallback().onMessageReceived(TEST_MESSAGE);
+        verify(mDelegateMessageCallback).onMessageReceived(TEST_MESSAGE);
+
+        doThrow(new RemoteException()).when(mDelegateMessageCallback).onMessageReceived(any());
+        tracker.getMessageCallback().onMessageReceived(TEST_MESSAGE);
+        verify(mISipDelegate).notifyMessageReceiveError(any(),
+                eq(SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_DEAD));
+    }
+
+    @SmallTest
+    @Test
+    public void testDelegateOnMessageReceivedClosedGracefully() throws Exception {
+        PendingExecutor executor = new PendingExecutor();
+        MessageTransportStateTracker tracker = new MessageTransportStateTracker(TEST_SUB_ID,
+                executor, mDelegateMessageCallback);
+        tracker.openTransport(mISipDelegate, Collections.emptySet());
+
+        tracker.getMessageCallback().onMessageReceived(TEST_MESSAGE);
+        executor.executePending();
+        verify(mDelegateMessageCallback).onMessageReceived(TEST_MESSAGE);
+
+        tracker.getMessageCallback().onMessageReceived(TEST_MESSAGE);
+        tracker.closeGracefully(
+                SipDelegateManager.MESSAGE_FAILURE_REASON_INTERNAL_DELEGATE_STATE_TRANSITION,
+                SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED,
+                mMockCloseConsumer);
+        executor.executePending();
+        // Incoming SIP message should not be blocked by closeGracefully
+        verify(mDelegateMessageCallback, times(2)).onMessageReceived(TEST_MESSAGE);
+    }
+
+    @SmallTest
+    @Test
+    public void testDelegateOnMessageSent() throws Exception {
+        MessageTransportStateTracker tracker = new MessageTransportStateTracker(TEST_SUB_ID,
+                Runnable::run, mDelegateMessageCallback);
+        tracker.openTransport(mISipDelegate, Collections.emptySet());
+        tracker.getMessageCallback().onMessageSent("z9hG4bK776asdhds");
+        verify(mDelegateMessageCallback).onMessageSent("z9hG4bK776asdhds");
+    }
+
+    @SmallTest
+    @Test
+    public void testDelegateonMessageSendFailure() throws Exception {
+        MessageTransportStateTracker tracker = new MessageTransportStateTracker(TEST_SUB_ID,
+                Runnable::run, mDelegateMessageCallback);
+        tracker.openTransport(mISipDelegate, Collections.emptySet());
+        tracker.getMessageCallback().onMessageSendFailure("z9hG4bK776asdhds",
+                SipDelegateManager.MESSAGE_FAILURE_REASON_NETWORK_NOT_AVAILABLE);
+        verify(mDelegateMessageCallback).onMessageSendFailure("z9hG4bK776asdhds",
+                SipDelegateManager.MESSAGE_FAILURE_REASON_NETWORK_NOT_AVAILABLE);
+    }
+}
diff --git a/tests/src/com/android/services/telephony/rcs/SipDelegateBinderConnectionTest.java b/tests/src/com/android/services/telephony/rcs/SipDelegateBinderConnectionTest.java
new file mode 100644
index 0000000..b95ae90
--- /dev/null
+++ b/tests/src/com/android/services/telephony/rcs/SipDelegateBinderConnectionTest.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2020 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.services.telephony.rcs;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
+
+import android.os.RemoteException;
+import android.telephony.ims.DelegateRegistrationState;
+import android.telephony.ims.DelegateRequest;
+import android.telephony.ims.FeatureTagState;
+import android.telephony.ims.SipDelegateImsConfiguration;
+import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.aidl.ISipDelegate;
+import android.telephony.ims.aidl.ISipDelegateMessageCallback;
+import android.telephony.ims.aidl.ISipDelegateStateCallback;
+import android.telephony.ims.aidl.ISipTransport;
+import android.util.ArraySet;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.TelephonyTestBase;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+
+import java.util.ArrayList;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+@RunWith(AndroidJUnit4.class)
+public class SipDelegateBinderConnectionTest extends TelephonyTestBase {
+    private static final int TEST_SUB_ID = 1;
+
+    @Mock private ISipDelegate mMockDelegate;
+    @Mock private ISipTransport mMockTransport;
+    @Mock private ISipDelegateMessageCallback mMessageCallback;
+    @Mock private DelegateBinderStateManager.StateCallback mMockStateCallback;
+    @Mock private BiConsumer<ISipDelegate, Set<FeatureTagState>> mMockCreatedCallback;
+    @Mock private Consumer<Integer> mMockDestroyedCallback;
+
+    private ArrayList<SipDelegateBinderConnection.StateCallback> mStateCallbackList;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        mStateCallbackList = new ArrayList<>(1);
+        mStateCallbackList.add(mMockStateCallback);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @SmallTest
+    @Test
+    public void testBaseImpl() throws Exception {
+        DelegateBinderStateManager baseConnection = new SipDelegateBinderConnectionStub(
+                getMmTelDeniedTag(), Runnable::run, mStateCallbackList);
+
+        baseConnection.create(null /*message cb*/, mMockCreatedCallback);
+        // Verify the stub simulates onCreated + on registration state callback.
+        verify(mMockCreatedCallback).accept(any(), eq(getMmTelDeniedTag()));
+        verify(mMockStateCallback).onRegistrationStateChanged(
+                new DelegateRegistrationState.Builder().build());
+
+        // Verify onDestroyed is called correctly.
+        baseConnection.destroy(SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP,
+                mMockDestroyedCallback);
+        verify(mMockDestroyedCallback).accept(
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+    }
+
+    @SmallTest
+    @Test
+    public void testCreateConnection() throws Exception {
+        DelegateRequest request = getDelegateRequest();
+        ArraySet<FeatureTagState> deniedTags = getMmTelDeniedTag();
+        SipDelegateBinderConnection connection = new SipDelegateBinderConnection(TEST_SUB_ID,
+                mMockTransport, request, deniedTags, Runnable::run, mStateCallbackList);
+        ISipDelegateStateCallback cb = createDelegateCaptureStateCallback(request, connection);
+
+        // Send onCreated callback from SipDelegate
+        ArrayList<FeatureTagState> delegateDeniedTags = new ArrayList<>(1);
+        delegateDeniedTags.add(new FeatureTagState(ImsSignallingUtils.GROUP_CHAT_TAG,
+                SipDelegateManager.DENIED_REASON_IN_USE_BY_ANOTHER_DELEGATE));
+        assertNotNull(cb);
+        cb.onCreated(mMockDelegate, delegateDeniedTags);
+
+        ArraySet<FeatureTagState> totalDeniedTags = new ArraySet<>(deniedTags);
+        // Add the tags denied by the controller as well.
+        totalDeniedTags.addAll(delegateDeniedTags);
+        // The callback should contain the controller and delegate denied tags in the callback.
+        verify(mMockCreatedCallback).accept(mMockDelegate, totalDeniedTags);
+    }
+
+    @SmallTest
+    @Test
+    public void testCreateConnectionServiceDead() throws Exception {
+        DelegateRequest request = getDelegateRequest();
+        ArraySet<FeatureTagState> deniedTags = getMmTelDeniedTag();
+        SipDelegateBinderConnection connection = new SipDelegateBinderConnection(TEST_SUB_ID,
+                mMockTransport, request, deniedTags, Runnable::run, mStateCallbackList);
+        doThrow(new RemoteException()).when(mMockTransport).createSipDelegate(eq(TEST_SUB_ID),
+                any(), any(), any());
+        ISipDelegateStateCallback cb = createDelegateCaptureStateCallback(request, connection);
+        assertNull(cb);
+    }
+
+    @SmallTest
+    @Test
+    public void testDestroyConnection() throws Exception {
+        DelegateRequest request = getDelegateRequest();
+        ArraySet<FeatureTagState> deniedTags = getMmTelDeniedTag();
+        SipDelegateBinderConnection connection = new SipDelegateBinderConnection(TEST_SUB_ID,
+                mMockTransport, request, deniedTags, Runnable::run, mStateCallbackList);
+        ISipDelegateStateCallback cb = createDelegateCaptureStateCallback(request, connection);
+        assertNotNull(cb);
+        cb.onCreated(mMockDelegate, null /*denied*/);
+        verify(mMockCreatedCallback).accept(mMockDelegate, deniedTags);
+
+        // call Destroy on the SipDelegate
+        destroy(connection, SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+        cb.onDestroyed(SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+        verify(mMockDestroyedCallback).accept(
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+    }
+
+    @SmallTest
+    @Test
+    public void testDestroyConnectionDead() throws Exception {
+        DelegateRequest request = getDelegateRequest();
+        ArraySet<FeatureTagState> deniedTags = getMmTelDeniedTag();
+        SipDelegateBinderConnection connection = new SipDelegateBinderConnection(TEST_SUB_ID,
+                mMockTransport, request, deniedTags, Runnable::run, mStateCallbackList);
+        ISipDelegateStateCallback cb = createDelegateCaptureStateCallback(request, connection);
+        assertNotNull(cb);
+        cb.onCreated(mMockDelegate, null /*denied*/);
+        verify(mMockCreatedCallback).accept(mMockDelegate, deniedTags);
+
+        // try to destroy when dead and ensure callback is still called.
+        doThrow(new RemoteException()).when(mMockTransport).destroySipDelegate(any(), anyInt());
+        destroy(connection, SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+        verify(mMockDestroyedCallback).accept(
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+    }
+
+    @SmallTest
+    @Test
+    public void testStateCallback() throws Exception {
+        DelegateRequest request = getDelegateRequest();
+        ArraySet<FeatureTagState> deniedTags = getMmTelDeniedTag();
+        SipDelegateBinderConnection connection = new SipDelegateBinderConnection(TEST_SUB_ID,
+                mMockTransport, request, deniedTags, Runnable::run, mStateCallbackList);
+        ISipDelegateStateCallback cb = createDelegateCaptureStateCallback(request, connection);
+        assertNotNull(cb);
+        cb.onCreated(mMockDelegate, new ArrayList<>(deniedTags));
+        verify(mMockCreatedCallback).accept(mMockDelegate, deniedTags);
+
+        SipDelegateImsConfiguration config = new SipDelegateImsConfiguration.Builder(1).build();
+        cb.onImsConfigurationChanged(config);
+        verify(mMockStateCallback).onImsConfigurationChanged(config);
+
+        DelegateRegistrationState regState = new DelegateRegistrationState.Builder()
+                .addRegisteredFeatureTags(request.getFeatureTags()).build();
+        cb.onFeatureTagRegistrationChanged(regState);
+        verify(mMockStateCallback).onRegistrationStateChanged(regState);
+    }
+
+    private ISipDelegateStateCallback createDelegateCaptureStateCallback(
+            DelegateRequest r, SipDelegateBinderConnection c) throws Exception {
+        boolean isCreating = c.create(mMessageCallback, mMockCreatedCallback);
+        if (!isCreating) return null;
+        ArgumentCaptor<ISipDelegateStateCallback> stateCaptor =
+                ArgumentCaptor.forClass(ISipDelegateStateCallback.class);
+        verify(mMockTransport).createSipDelegate(eq(TEST_SUB_ID), eq(r), stateCaptor.capture(),
+                eq(mMessageCallback));
+        assertNotNull(stateCaptor.getValue());
+        return stateCaptor.getValue();
+    }
+
+    private void destroy(SipDelegateBinderConnection c, int reason) throws Exception {
+        c.destroy(reason, mMockDestroyedCallback);
+        verify(mMockTransport).destroySipDelegate(mMockDelegate, reason);
+    }
+
+    private DelegateRequest getDelegateRequest() {
+        ArraySet<String> featureTags = new ArraySet<>(2);
+        featureTags.add(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG);
+        featureTags.add(ImsSignallingUtils.GROUP_CHAT_TAG);
+        return new DelegateRequest(featureTags);
+    }
+
+    private ArraySet<FeatureTagState> getMmTelDeniedTag() {
+        ArraySet<FeatureTagState> deniedTags = new ArraySet<>();
+        deniedTags.add(new FeatureTagState(ImsSignallingUtils.MMTEL_TAG,
+                SipDelegateManager.DENIED_REASON_NOT_ALLOWED));
+        return deniedTags;
+    }
+}
diff --git a/tests/src/com/android/services/telephony/rcs/SipDelegateControllerTest.java b/tests/src/com/android/services/telephony/rcs/SipDelegateControllerTest.java
new file mode 100644
index 0000000..47b4808
--- /dev/null
+++ b/tests/src/com/android/services/telephony/rcs/SipDelegateControllerTest.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2020 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.services.telephony.rcs;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.telephony.ims.DelegateRegistrationState;
+import android.telephony.ims.DelegateRequest;
+import android.telephony.ims.FeatureTagState;
+import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.aidl.ISipDelegate;
+import android.telephony.ims.aidl.ISipDelegateMessageCallback;
+import android.telephony.ims.aidl.ISipTransport;
+import android.util.ArraySet;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.TelephonyTestBase;
+import com.android.TestExecutorService;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+@RunWith(AndroidJUnit4.class)
+public class SipDelegateControllerTest extends TelephonyTestBase {
+    private static final int TEST_SUB_ID = 1;
+
+    @Mock private ISipDelegate mMockSipDelegate;
+    @Mock private ISipTransport mMockSipTransport;
+    @Mock private MessageTransportStateTracker mMockMessageTracker;
+    @Mock private ISipDelegateMessageCallback mMockMessageCallback;
+    @Mock private DelegateStateTracker mMockDelegateStateTracker;
+    @Mock private DelegateBinderStateManager mMockBinderConnection;
+    @Captor private ArgumentCaptor<BiConsumer<ISipDelegate, Set<FeatureTagState>>> mCreatedCaptor;
+    @Captor private ArgumentCaptor<Consumer<Boolean>> mBooleanConsumerCaptor;
+    @Captor private ArgumentCaptor<Consumer<Integer>> mIntegerConsumerCaptor;
+
+    private ScheduledExecutorService mExecutorService;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        when(mMockMessageTracker.getMessageCallback()).thenReturn(mMockMessageCallback);
+        mExecutorService = new TestExecutorService();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mExecutorService.shutdownNow();
+        super.tearDown();
+    }
+
+    @SmallTest
+    @Test
+    public void testCreateDelegate() throws Exception {
+        DelegateRequest request = getBaseDelegateRequest();
+        SipDelegateController controller = getTestDelegateController(request,
+                Collections.emptySet());
+
+        doReturn(true).when(mMockBinderConnection).create(eq(mMockMessageCallback), any());
+        CompletableFuture<Boolean> future = controller.create(request.getFeatureTags(),
+                Collections.emptySet() /*denied tags*/);
+        BiConsumer<ISipDelegate, Set<FeatureTagState>> consumer =
+                verifyConnectionCreated(1);
+        assertNotNull(consumer);
+
+        assertFalse(future.isDone());
+        consumer.accept(mMockSipDelegate, Collections.emptySet());
+        assertTrue(future.get());
+        verify(mMockMessageTracker).openTransport(mMockSipDelegate, Collections.emptySet());
+        verify(mMockDelegateStateTracker).sipDelegateConnected(Collections.emptySet());
+    }
+
+    @SmallTest
+    @Test
+    public void testCreateDelegateTransportDied() throws Exception {
+        DelegateRequest request = getBaseDelegateRequest();
+        SipDelegateController controller = getTestDelegateController(request,
+                Collections.emptySet());
+
+        //Create operation fails
+        doReturn(false).when(mMockBinderConnection).create(eq(mMockMessageCallback), any());
+        CompletableFuture<Boolean> future = controller.create(request.getFeatureTags(),
+                Collections.emptySet() /*denied tags*/);
+
+        assertFalse(future.get());
+    }
+
+    @SmallTest
+    @Test
+    public void testDestroyDelegate() throws Exception {
+        DelegateRequest request = getBaseDelegateRequest();
+        SipDelegateController controller = getTestDelegateController(request,
+                Collections.emptySet());
+        createSipDelegate(request, controller);
+
+        CompletableFuture<Integer> pendingDestroy = controller.destroy(false /*force*/,
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+        assertFalse(pendingDestroy.isDone());
+        Consumer<Boolean> pendingClosedConsumer = verifyMessageTrackerCloseGracefully();
+        verify(mMockDelegateStateTracker).sipDelegateChanging(
+                DelegateRegistrationState.DEREGISTERING_REASON_DESTROY_PENDING);
+
+        // verify we do not call destroy on the delegate until the message tracker releases the
+        // transport.
+        verify(mMockBinderConnection, never()).destroy(anyInt(), any());
+        pendingClosedConsumer.accept(true);
+        Consumer<Integer> pendingDestroyedConsumer = verifyBinderConnectionDestroy();
+        pendingDestroyedConsumer.accept(
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+        verify(mMockDelegateStateTracker).sipDelegateDestroyed(
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+        assertTrue(pendingDestroy.isDone());
+        assertEquals(SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP,
+                pendingDestroy.get().intValue());
+    }
+
+    @SmallTest
+    @Test
+    public void testDestroyDelegateForce() throws Exception {
+        DelegateRequest request = getBaseDelegateRequest();
+        SipDelegateController controller = getTestDelegateController(request,
+                Collections.emptySet());
+        createSipDelegate(request, controller);
+
+        CompletableFuture<Integer> pendingDestroy = controller.destroy(true /*force*/,
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+        assertFalse(pendingDestroy.isDone());
+        // Do not wait for message transport close in this case.
+        verify(mMockMessageTracker).close(
+                SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED);
+        verify(mMockDelegateStateTracker, never()).sipDelegateChanging(
+                DelegateRegistrationState.DEREGISTERING_REASON_DESTROY_PENDING);
+
+        //verify destroy is called
+        Consumer<Integer> pendingDestroyedConsumer = verifyBinderConnectionDestroy();
+        pendingDestroyedConsumer.accept(
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+        verify(mMockDelegateStateTracker).sipDelegateDestroyed(
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+        assertTrue(pendingDestroy.isDone());
+        assertEquals(SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP,
+                pendingDestroy.get().intValue());
+    }
+
+    @SmallTest
+    @Test
+    public void testChangeSupportedFeatures() throws Exception {
+        DelegateRequest request = getBaseDelegateRequest();
+        SipDelegateController controller = getTestDelegateController(request,
+                Collections.emptySet());
+        createSipDelegate(request, controller);
+
+        Set<String> newFts = getBaseFTSet();
+        newFts.add(ImsSignallingUtils.GROUP_CHAT_TAG);
+        CompletableFuture<Boolean> pendingChange = controller.changeSupportedFeatureTags(
+                newFts, Collections.emptySet());
+        assertFalse(pendingChange.isDone());
+        // message tracker should close gracefully.
+        Consumer<Boolean> pendingClosedConsumer = verifyMessageTrackerCloseGracefully();
+        verify(mMockDelegateStateTracker).sipDelegateChanging(
+                DelegateRegistrationState.DEREGISTERING_REASON_FEATURE_TAGS_CHANGING);
+        verify(mMockBinderConnection, never()).destroy(anyInt(), any());
+        pendingClosedConsumer.accept(true);
+        Consumer<Integer> pendingDestroyedConsumer = verifyBinderConnectionDestroy();
+        pendingDestroyedConsumer.accept(
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+        verify(mMockDelegateStateTracker, never()).sipDelegateDestroyed(anyInt());
+
+        // This will cause any exceptions to be printed if something completed exceptionally.
+        assertNull(pendingChange.getNow(null));
+        BiConsumer<ISipDelegate, Set<FeatureTagState>> consumer =
+                verifyConnectionCreated(2);
+        assertNotNull(consumer);
+        consumer.accept(mMockSipDelegate, Collections.emptySet());
+        assertTrue(pendingChange.get());
+
+        verify(mMockMessageTracker, times(2)).openTransport(mMockSipDelegate,
+                Collections.emptySet());
+        verify(mMockDelegateStateTracker, times(2)).sipDelegateConnected(Collections.emptySet());
+    }
+
+    private void createSipDelegate(DelegateRequest request, SipDelegateController controller)
+            throws Exception {
+        doReturn(true).when(mMockBinderConnection).create(eq(mMockMessageCallback), any());
+        CompletableFuture<Boolean> future = controller.create(request.getFeatureTags(),
+                Collections.emptySet() /*denied tags*/);
+        BiConsumer<ISipDelegate, Set<FeatureTagState>> consumer =
+                verifyConnectionCreated(1);
+        assertNotNull(consumer);
+        consumer.accept(mMockSipDelegate, Collections.emptySet());
+        assertTrue(future.get());
+    }
+
+    private ArraySet<String> getBaseFTSet() {
+        ArraySet<String> request = new ArraySet<>();
+        request.add(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG);
+        return request;
+    }
+
+    private DelegateRequest getBaseDelegateRequest() {
+        return new DelegateRequest(getBaseFTSet());
+    }
+
+    private SipDelegateController getTestDelegateController(DelegateRequest request,
+            Set<FeatureTagState> deniedSet) {
+        return new SipDelegateController(TEST_SUB_ID, request, "", mMockSipTransport,
+                mExecutorService, mMockMessageTracker, mMockDelegateStateTracker,
+                (a, b, c, deniedFeatureSet, e, f) ->  {
+                    assertEquals(deniedSet, deniedFeatureSet);
+                    return mMockBinderConnection;
+                });
+    }
+
+    private BiConsumer<ISipDelegate, Set<FeatureTagState>> verifyConnectionCreated(int numTimes) {
+        verify(mMockBinderConnection, times(numTimes)).create(eq(mMockMessageCallback),
+                mCreatedCaptor.capture());
+        return mCreatedCaptor.getValue();
+    }
+
+    private Consumer<Boolean> verifyMessageTrackerCloseGracefully() {
+        verify(mMockMessageTracker).closeGracefully(anyInt(), anyInt(),
+                mBooleanConsumerCaptor.capture());
+        return mBooleanConsumerCaptor.getValue();
+    }
+    private Consumer<Integer> verifyBinderConnectionDestroy() {
+        verify(mMockBinderConnection).destroy(anyInt(), mIntegerConsumerCaptor.capture());
+        return mIntegerConsumerCaptor.getValue();
+    }
+
+}
diff --git a/tests/src/com/android/services/telephony/rcs/SipTransportControllerTest.java b/tests/src/com/android/services/telephony/rcs/SipTransportControllerTest.java
index 65a95cd..8e10757 100644
--- a/tests/src/com/android/services/telephony/rcs/SipTransportControllerTest.java
+++ b/tests/src/com/android/services/telephony/rcs/SipTransportControllerTest.java
@@ -18,13 +18,34 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 
+import android.app.role.RoleManager;
+import android.os.IBinder;
+import android.os.UserHandle;
+import android.telephony.ims.DelegateRequest;
+import android.telephony.ims.FeatureTagState;
 import android.telephony.ims.ImsException;
+import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.aidl.ISipDelegate;
+import android.telephony.ims.aidl.ISipDelegateConnectionStateCallback;
+import android.telephony.ims.aidl.ISipDelegateMessageCallback;
 import android.telephony.ims.aidl.ISipTransport;
+import android.util.ArraySet;
+import android.util.Pair;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -39,30 +60,89 @@
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
 @RunWith(AndroidJUnit4.class)
 public class SipTransportControllerTest extends TelephonyTestBase {
+    private static final int TEST_SUB_ID = 1;
+    private static final String TEST_PACKAGE_NAME = "com.test_pkg";
+    private static final String TEST_PACKAGE_NAME_2 = "com.test_pkg2";
+    private static final int TIMEOUT_MS = 200;
+    private static final int THROTTLE_MS = 50;
+
+    private class SipDelegateControllerContainer {
+        public final int subId;
+        public final String packageName;
+        public final DelegateRequest delegateRequest;
+        public final SipDelegateController delegateController;
+        public final ISipDelegate mMockDelegate;
+        public final IBinder mMockDelegateBinder;
+
+        SipDelegateControllerContainer(int id, String name, DelegateRequest request) {
+            delegateController = mock(SipDelegateController.class);
+            mMockDelegate = mock(ISipDelegate.class);
+            mMockDelegateBinder = mock(IBinder.class);
+            doReturn(mMockDelegateBinder).when(mMockDelegate).asBinder();
+            doReturn(name).when(delegateController).getPackageName();
+            doReturn(request).when(delegateController).getInitialRequest();
+            doReturn(mMockDelegate).when(delegateController).getSipDelegateInterface();
+            subId = id;
+            packageName = name;
+            delegateRequest = request;
+        }
+    }
 
     @Mock private RcsFeatureManager mRcsManager;
     @Mock private ISipTransport mSipTransport;
+    @Mock private ISipDelegateConnectionStateCallback mMockStateCallback;
+    @Mock private ISipDelegateMessageCallback mMockMessageCallback;
+    @Mock private SipTransportController.SipDelegateControllerFactory
+            mMockDelegateControllerFactory;
+    @Mock private SipTransportController.RoleManagerAdapter mMockRoleManager;
 
-    private final TestExecutorService mExecutorService = new TestExecutorService();
+    private ScheduledExecutorService mExecutorService = null;
+    private final ArrayList<SipDelegateControllerContainer> mMockControllers = new ArrayList<>();
+    private final ArrayList<String> mSmsPackageName = new ArrayList<>(1);
 
     @Before
     public void setUp() throws Exception {
         super.setUp();
+        doReturn(mSmsPackageName).when(mMockRoleManager).getRoleHolders(RoleManager.ROLE_SMS);
+        mSmsPackageName.add(TEST_PACKAGE_NAME);
+        doAnswer(invocation -> {
+            Integer subId = invocation.getArgument(0);
+            String packageName = invocation.getArgument(2);
+            DelegateRequest request = invocation.getArgument(1);
+            SipDelegateController c = getMockDelegateController(subId, packageName, request);
+            assertNotNull("create called with no corresponding controller set up", c);
+            return c;
+        }).when(mMockDelegateControllerFactory).create(anyInt(), any(), anyString(), any(),
+                any(), any(), any());
     }
 
     @After
     public void tearDown() throws Exception {
         super.tearDown();
+        boolean isShutdown = mExecutorService == null || mExecutorService.isShutdown();
+        if (!isShutdown) {
+            mExecutorService.shutdownNow();
+        }
     }
 
     @SmallTest
     @Test
     public void isSupportedRcsNotConnected() {
-        SipTransportController controller = createController(0 /*slotId*/, 0 /*subId*/);
+        SipTransportController controller = createController(new TestExecutorService());
         try {
-            controller.isSupported(0 /*subId*/);
+            controller.isSupported(TEST_SUB_ID);
             fail();
         } catch (ImsException e) {
             assertEquals(ImsException.CODE_ERROR_SERVICE_UNAVAILABLE, e.getCode());
@@ -72,9 +152,9 @@
     @SmallTest
     @Test
     public void isSupportedInvalidSubId() {
-        SipTransportController controller = createController(0 /*slotId*/, 0 /*subId*/);
+        SipTransportController controller = createController(new TestExecutorService());
         try {
-            controller.isSupported(1 /*subId*/);
+            controller.isSupported(TEST_SUB_ID + 1);
             fail();
         } catch (ImsException e) {
             assertEquals(ImsException.CODE_ERROR_INVALID_SUBSCRIPTION, e.getCode());
@@ -84,10 +164,10 @@
     @SmallTest
     @Test
     public void isSupportedSubIdChanged() {
-        SipTransportController controller = createController(0 /*slotId*/, 0 /*subId*/);
-        controller.onAssociatedSubscriptionUpdated(1 /*subId*/);
+        SipTransportController controller = createController(new TestExecutorService());
+        controller.onAssociatedSubscriptionUpdated(TEST_SUB_ID + 1);
         try {
-            controller.isSupported(0 /*subId*/);
+            controller.isSupported(TEST_SUB_ID);
             fail();
         } catch (ImsException e) {
             assertEquals(ImsException.CODE_ERROR_INVALID_SUBSCRIPTION, e.getCode());
@@ -97,11 +177,11 @@
     @SmallTest
     @Test
     public void isSupportedSipTransportAvailableRcsConnected() throws Exception {
-        SipTransportController controller = createController(0 /*slotId*/, 0 /*subId*/);
+        SipTransportController controller = createController(new TestExecutorService());
         doReturn(mSipTransport).when(mRcsManager).getSipTransport();
         controller.onRcsConnected(mRcsManager);
         try {
-            assertTrue(controller.isSupported(0 /*subId*/));
+            assertTrue(controller.isSupported(TEST_SUB_ID));
         } catch (ImsException e) {
             fail();
         }
@@ -110,12 +190,12 @@
     @SmallTest
     @Test
     public void isSupportedSipTransportNotAvailableRcsDisconnected() throws Exception {
-        SipTransportController controller = createController(0 /*slotId*/, 0 /*subId*/);
+        SipTransportController controller = createController(new TestExecutorService());
         doReturn(mSipTransport).when(mRcsManager).getSipTransport();
         controller.onRcsConnected(mRcsManager);
         controller.onRcsDisconnected();
         try {
-            controller.isSupported(0 /*subId*/);
+            controller.isSupported(TEST_SUB_ID);
             fail();
         } catch (ImsException e) {
             assertEquals(ImsException.CODE_ERROR_SERVICE_UNAVAILABLE, e.getCode());
@@ -125,11 +205,11 @@
     @SmallTest
     @Test
     public void isSupportedSipTransportNotAvailableRcsConnected() throws Exception {
-        SipTransportController controller = createController(0 /*slotId*/, 0 /*subId*/);
+        SipTransportController controller = createController(new TestExecutorService());
         doReturn(null).when(mRcsManager).getSipTransport();
         controller.onRcsConnected(mRcsManager);
         try {
-            assertFalse(controller.isSupported(0 /*subId*/));
+            assertFalse(controller.isSupported(TEST_SUB_ID));
         } catch (ImsException e) {
             fail();
         }
@@ -138,19 +218,627 @@
     @SmallTest
     @Test
     public void isSupportedImsServiceNotAvailableRcsConnected() throws Exception {
-        SipTransportController controller = createController(0 /*slotId*/, 0 /*subId*/);
+        SipTransportController controller = createController(new TestExecutorService());
         doThrow(new ImsException("", ImsException.CODE_ERROR_SERVICE_UNAVAILABLE))
                 .when(mRcsManager).getSipTransport();
         controller.onRcsConnected(mRcsManager);
         try {
-            controller.isSupported(0 /*subId*/);
+            controller.isSupported(TEST_SUB_ID);
             fail();
         } catch (ImsException e) {
             assertEquals(ImsException.CODE_ERROR_SERVICE_UNAVAILABLE, e.getCode());
         }
     }
 
-    private SipTransportController createController(int slotId, int subId) {
-        return new SipTransportController(mContext, slotId, subId, mExecutorService);
+    @SmallTest
+    @Test
+    public void createImsServiceAvailableSubIdIncorrect() throws Exception {
+        SipTransportController controller = createController(new TestExecutorService());
+        doReturn(mSipTransport).when(mRcsManager).getSipTransport();
+        controller.onRcsConnected(mRcsManager);
+        try {
+            controller.createSipDelegate(TEST_SUB_ID + 1,
+                    new DelegateRequest(Collections.emptySet()), TEST_PACKAGE_NAME,
+                    mMockStateCallback, mMockMessageCallback);
+            fail();
+        } catch (ImsException e) {
+            assertEquals(ImsException.CODE_ERROR_INVALID_SUBSCRIPTION, e.getCode());
+        }
+    }
+
+    @SmallTest
+    @Test
+    public void createImsServiceDoesntSupportTransport() throws Exception {
+        SipTransportController controller = createController(new TestExecutorService());
+        doReturn(null).when(mRcsManager).getSipTransport();
+        controller.onRcsConnected(mRcsManager);
+        try {
+            controller.createSipDelegate(TEST_SUB_ID,
+                    new DelegateRequest(Collections.emptySet()), TEST_PACKAGE_NAME,
+                    mMockStateCallback, mMockMessageCallback);
+            fail();
+        } catch (ImsException e) {
+            assertEquals(ImsException.CODE_ERROR_UNSUPPORTED_OPERATION, e.getCode());
+        }
+    }
+
+    @SmallTest
+    @Test
+    public void createImsServiceNotAvailable() throws Exception {
+        SipTransportController controller = createController(new TestExecutorService());
+        doThrow(new ImsException("", ImsException.CODE_ERROR_SERVICE_UNAVAILABLE))
+                .when(mRcsManager).getSipTransport();
+        // No RCS connected message
+        try {
+            controller.createSipDelegate(TEST_SUB_ID,
+                    new DelegateRequest(Collections.emptySet()), TEST_PACKAGE_NAME,
+                    mMockStateCallback, mMockMessageCallback);
+            fail();
+        } catch (ImsException e) {
+            assertEquals(ImsException.CODE_ERROR_SERVICE_UNAVAILABLE, e.getCode());
+        }
+    }
+
+    @SmallTest
+    @Test
+    public void basicCreate() throws Exception {
+        SipTransportController controller = setupLiveTransportController();
+
+        DelegateRequest r = getBaseDelegateRequest();
+
+        SipDelegateController c = injectMockDelegateController(TEST_PACKAGE_NAME, r);
+        createDelegateAndVerify(controller, c, r, r.getFeatureTags(), Collections.emptySet(),
+                TEST_PACKAGE_NAME);
+    }
+
+    @SmallTest
+    @Test
+    public void basicCreateDestroy() throws Exception {
+        SipTransportController controller = setupLiveTransportController();
+
+        DelegateRequest r = getBaseDelegateRequest();
+        SipDelegateController c = injectMockDelegateController(TEST_PACKAGE_NAME, r);
+        createDelegateAndVerify(controller, c, r, r.getFeatureTags(), Collections.emptySet(),
+                TEST_PACKAGE_NAME);
+
+        destroyDelegateAndVerify(controller, c, false,
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+    }
+
+    @SmallTest
+    @Test
+    public void testCreateButNotInRole() throws Exception {
+        SipTransportController controller = setupLiveTransportController();
+
+        DelegateRequest r = getBaseDelegateRequest();
+        Set<FeatureTagState> getDeniedTags = getDeniedTagsForReason(r.getFeatureTags(),
+                SipDelegateManager.DENIED_REASON_NOT_ALLOWED);
+
+        // Try to create a SipDelegate for a package that is not the default sms role.
+        SipDelegateController c = injectMockDelegateController(TEST_PACKAGE_NAME_2, r);
+        createDelegateAndVerify(controller, c, r, Collections.emptySet(), getDeniedTags,
+                TEST_PACKAGE_NAME_2);
+    }
+
+    @SmallTest
+    @Test
+    public void createTwoAndDenyOverlappingTags() throws Exception {
+        SipTransportController controller = setupLiveTransportController();
+
+        // First delegate requests RCS message + File transfer
+        ArraySet<String> firstDelegate = new ArraySet<>(getBaseDelegateRequest().getFeatureTags());
+        firstDelegate.remove(ImsSignallingUtils.GROUP_CHAT_TAG);
+        DelegateRequest firstDelegateRequest = new DelegateRequest(firstDelegate);
+        SipDelegateController c1 = injectMockDelegateController(TEST_PACKAGE_NAME,
+                firstDelegateRequest);
+        createDelegateAndVerify(controller, c1, firstDelegateRequest, firstDelegate,
+                Collections.emptySet(), TEST_PACKAGE_NAME);
+
+        // First delegate requests RCS message + Group RCS message. For this delegate, single RCS
+        // message should be denied.
+        ArraySet<String> secondDelegate = new ArraySet<>(getBaseDelegateRequest().getFeatureTags());
+        secondDelegate.remove(ImsSignallingUtils.FILE_TRANSFER_HTTP_TAG);
+        DelegateRequest secondDelegateRequest = new DelegateRequest(secondDelegate);
+        Pair<Set<String>, Set<FeatureTagState>> grantedAndDenied = getAllowedAndDeniedTagsForConfig(
+                secondDelegateRequest, SipDelegateManager.DENIED_REASON_IN_USE_BY_ANOTHER_DELEGATE,
+                firstDelegate);
+        SipDelegateController c2 = injectMockDelegateController(TEST_PACKAGE_NAME,
+                secondDelegateRequest);
+        createDelegateAndVerify(controller, c2, secondDelegateRequest, grantedAndDenied.first,
+                grantedAndDenied.second, TEST_PACKAGE_NAME, 1);
+    }
+
+    @SmallTest
+    @Test
+    public void createTwoAndTriggerRoleChange() throws Exception {
+        SipTransportController controller = setupLiveTransportController();
+
+        DelegateRequest firstDelegateRequest = getBaseDelegateRequest();
+        Set<FeatureTagState> firstDeniedTags = getDeniedTagsForReason(
+                firstDelegateRequest.getFeatureTags(),
+                SipDelegateManager.DENIED_REASON_NOT_ALLOWED);
+        SipDelegateController c1 = injectMockDelegateController(TEST_PACKAGE_NAME,
+                firstDelegateRequest);
+        createDelegateAndVerify(controller, c1, firstDelegateRequest,
+                firstDelegateRequest.getFeatureTags(), Collections.emptySet(), TEST_PACKAGE_NAME);
+
+        DelegateRequest secondDelegateRequest = getBaseDelegateRequest();
+        Set<FeatureTagState> secondDeniedTags = getDeniedTagsForReason(
+                secondDelegateRequest.getFeatureTags(),
+                SipDelegateManager.DENIED_REASON_NOT_ALLOWED);
+        // Try to create a SipDelegate for a package that is not the default sms role.
+        SipDelegateController c2 = injectMockDelegateController(TEST_PACKAGE_NAME_2,
+                secondDelegateRequest);
+        createDelegateAndVerify(controller, c2, secondDelegateRequest, Collections.emptySet(),
+                secondDeniedTags, TEST_PACKAGE_NAME_2, 1);
+
+        // now swap the SMS role.
+        CompletableFuture<Boolean> pendingC1Change = setChangeSupportedFeatureTagsFuture(c1,
+                Collections.emptySet(), firstDeniedTags);
+        CompletableFuture<Boolean> pendingC2Change = setChangeSupportedFeatureTagsFuture(c2,
+                secondDelegateRequest.getFeatureTags(), Collections.emptySet());
+        setSmsRoleAndEvaluate(controller, TEST_PACKAGE_NAME_2);
+        // trigger completion stage to run
+        waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+        verify(c1).changeSupportedFeatureTags(Collections.emptySet(), firstDeniedTags);
+        // we should not get a change for c2 until pendingC1Change completes.
+        verify(c2, never()).changeSupportedFeatureTags(secondDelegateRequest.getFeatureTags(),
+                Collections.emptySet());
+        // ensure we are not blocking executor here
+        waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+        completePendingChange(pendingC1Change, true);
+        // trigger completion stage to run
+        waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+        verify(c2).changeSupportedFeatureTags(secondDelegateRequest.getFeatureTags(),
+                Collections.emptySet());
+        // ensure we are not blocking executor here
+        waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+        completePendingChange(pendingC2Change, true);
+    }
+
+    @SmallTest
+    @Test
+    public void createTwoAndDestroyOlder() throws Exception {
+        SipTransportController controller = setupLiveTransportController();
+
+        // First delegate requests RCS message + File transfer
+        ArraySet<String> firstDelegate = new ArraySet<>(getBaseDelegateRequest().getFeatureTags());
+        firstDelegate.remove(ImsSignallingUtils.GROUP_CHAT_TAG);
+        DelegateRequest firstDelegateRequest = new DelegateRequest(firstDelegate);
+        SipDelegateController c1 = injectMockDelegateController(TEST_PACKAGE_NAME,
+                firstDelegateRequest);
+        createDelegateAndVerify(controller, c1, firstDelegateRequest, firstDelegate,
+                Collections.emptySet(), TEST_PACKAGE_NAME);
+
+        // First delegate requests RCS message + Group RCS message. For this delegate, single RCS
+        // message should be denied.
+        ArraySet<String> secondDelegate = new ArraySet<>(getBaseDelegateRequest().getFeatureTags());
+        secondDelegate.remove(ImsSignallingUtils.FILE_TRANSFER_HTTP_TAG);
+        DelegateRequest secondDelegateRequest = new DelegateRequest(secondDelegate);
+        Pair<Set<String>, Set<FeatureTagState>> grantedAndDenied = getAllowedAndDeniedTagsForConfig(
+                secondDelegateRequest, SipDelegateManager.DENIED_REASON_IN_USE_BY_ANOTHER_DELEGATE,
+                firstDelegate);
+        SipDelegateController c2 = injectMockDelegateController(TEST_PACKAGE_NAME,
+                secondDelegateRequest);
+        createDelegateAndVerify(controller, c2, secondDelegateRequest, grantedAndDenied.first,
+                grantedAndDenied.second, TEST_PACKAGE_NAME, 1);
+
+        // Destroy the firstDelegate, which should now cause all previously denied tags to be
+        // granted to the new delegate.
+        CompletableFuture<Boolean> pendingC2Change = setChangeSupportedFeatureTagsFuture(c2,
+                secondDelegate, Collections.emptySet());
+        destroyDelegateAndVerify(controller, c1, false /*force*/,
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+        // wait for create to be processed.
+        assertTrue(waitForExecutorAction(mExecutorService, TIMEOUT_MS));
+        verify(c2).changeSupportedFeatureTags(secondDelegate, Collections.emptySet());
+        completePendingChange(pendingC2Change, true);
+    }
+
+    @SmallTest
+    @Test
+    public void testThrottling() throws Exception {
+        SipTransportController controller = setupLiveTransportController(THROTTLE_MS);
+
+        // First delegate requests RCS message + File transfer
+        ArraySet<String> firstDelegate = new ArraySet<>(getBaseDelegateRequest().getFeatureTags());
+        firstDelegate.remove(ImsSignallingUtils.GROUP_CHAT_TAG);
+        DelegateRequest firstDelegateRequest = new DelegateRequest(firstDelegate);
+        SipDelegateController c1 = injectMockDelegateController(TEST_PACKAGE_NAME,
+                firstDelegateRequest);
+        CompletableFuture<Boolean> pendingC1Change = createDelegate(controller, c1,
+                firstDelegateRequest, firstDelegate, Collections.emptySet(), TEST_PACKAGE_NAME);
+
+        // Request RCS message + group RCS Message. For this delegate, single RCS message should be
+        // denied.
+        ArraySet<String> secondDelegate = new ArraySet<>(getBaseDelegateRequest().getFeatureTags());
+        secondDelegate.remove(ImsSignallingUtils.FILE_TRANSFER_HTTP_TAG);
+        DelegateRequest secondDelegateRequest = new DelegateRequest(secondDelegate);
+        Pair<Set<String>, Set<FeatureTagState>> grantedAndDeniedC2 =
+                getAllowedAndDeniedTagsForConfig(secondDelegateRequest,
+                        SipDelegateManager.DENIED_REASON_IN_USE_BY_ANOTHER_DELEGATE, firstDelegate);
+        SipDelegateController c2 = injectMockDelegateController(TEST_PACKAGE_NAME,
+                secondDelegateRequest);
+        CompletableFuture<Boolean> pendingC2Change = createDelegate(controller, c2,
+                secondDelegateRequest, grantedAndDeniedC2.first, grantedAndDeniedC2.second,
+                TEST_PACKAGE_NAME);
+
+        // Request group RCS message + file transfer. All should be denied at first
+        ArraySet<String> thirdDelegate = new ArraySet<>(getBaseDelegateRequest().getFeatureTags());
+        thirdDelegate.remove(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG);
+        DelegateRequest thirdDelegateRequest = new DelegateRequest(thirdDelegate);
+        Pair<Set<String>, Set<FeatureTagState>> grantedAndDeniedC3 =
+                getAllowedAndDeniedTagsForConfig(thirdDelegateRequest,
+                        SipDelegateManager.DENIED_REASON_IN_USE_BY_ANOTHER_DELEGATE, firstDelegate,
+                        grantedAndDeniedC2.first);
+        SipDelegateController c3 = injectMockDelegateController(TEST_PACKAGE_NAME,
+                thirdDelegateRequest);
+        CompletableFuture<Boolean> pendingC3Change = createDelegate(controller, c3,
+                thirdDelegateRequest, grantedAndDeniedC3.first, grantedAndDeniedC3.second,
+                TEST_PACKAGE_NAME);
+
+        assertTrue(scheduleDelayedWait(2 * THROTTLE_MS));
+        verifyDelegateChanged(c1, pendingC1Change, firstDelegate, Collections.emptySet(), 0);
+        verifyDelegateChanged(c2, pendingC2Change, grantedAndDeniedC2.first,
+                grantedAndDeniedC2.second, 0);
+        verifyDelegateChanged(c3, pendingC3Change, grantedAndDeniedC3.first,
+                grantedAndDeniedC3.second, 0);
+
+        // Destroy the first and second controller in quick succession, this should only generate
+        // one reevaluate for the third controller.
+        CompletableFuture<Boolean> pendingChangeC3 = setChangeSupportedFeatureTagsFuture(
+                c3, thirdDelegate, Collections.emptySet());
+        CompletableFuture<Integer> pendingDestroyC1 = destroyDelegate(controller, c1,
+                false /*force*/,
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+        CompletableFuture<Integer> pendingDestroyC2 = destroyDelegate(controller, c2,
+                false /*force*/,
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+        assertTrue(scheduleDelayedWait(2 * THROTTLE_MS));
+        verifyDestroyDelegate(controller, c1, pendingDestroyC1, false /*force*/,
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+        verifyDestroyDelegate(controller, c2, pendingDestroyC2, false /*force*/,
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+
+        // All requested features should now be granted
+        completePendingChange(pendingChangeC3, true);
+        verify(c3).changeSupportedFeatureTags(thirdDelegate, Collections.emptySet());
+        // In total reeval should have only been called twice.
+        verify(c3, times(2)).changeSupportedFeatureTags(any(), any());
+    }
+
+    @SmallTest
+    @Test
+    public void testSubIdChangeDestroyTriggered() throws Exception {
+        SipTransportController controller = setupLiveTransportController();
+
+        ArraySet<String> firstDelegate = new ArraySet<>(getBaseDelegateRequest().getFeatureTags());
+        DelegateRequest firstDelegateRequest = new DelegateRequest(firstDelegate);
+        SipDelegateController c1 = injectMockDelegateController(TEST_PACKAGE_NAME,
+                firstDelegateRequest);
+        createDelegateAndVerify(controller, c1, firstDelegateRequest, firstDelegate,
+                Collections.emptySet(), TEST_PACKAGE_NAME);
+
+        CompletableFuture<Integer> pendingDestroy =  setDestroyFuture(c1, true,
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SUBSCRIPTION_TORN_DOWN);
+        controller.onAssociatedSubscriptionUpdated(TEST_SUB_ID + 1);
+        waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+        verifyDestroyDelegate(controller, c1, pendingDestroy, true /*force*/,
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SUBSCRIPTION_TORN_DOWN);
+    }
+
+    @SmallTest
+    @Test
+    public void testRcsManagerGoneDestroyTriggered() throws Exception {
+        SipTransportController controller = setupLiveTransportController();
+
+        ArraySet<String> firstDelegate = new ArraySet<>(getBaseDelegateRequest().getFeatureTags());
+        DelegateRequest firstDelegateRequest = new DelegateRequest(firstDelegate);
+        SipDelegateController c1 = injectMockDelegateController(TEST_PACKAGE_NAME,
+                firstDelegateRequest);
+        createDelegateAndVerify(controller, c1, firstDelegateRequest, firstDelegate,
+                Collections.emptySet(), TEST_PACKAGE_NAME);
+
+        CompletableFuture<Integer> pendingDestroy =  setDestroyFuture(c1, true,
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SERVICE_DEAD);
+        controller.onRcsDisconnected();
+        waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+        verifyDestroyDelegate(controller, c1, pendingDestroy, true /*force*/,
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SERVICE_DEAD);
+    }
+
+    @SmallTest
+    @Test
+    public void testDestroyTriggered() throws Exception {
+        SipTransportController controller = setupLiveTransportController();
+
+        ArraySet<String> firstDelegate = new ArraySet<>(getBaseDelegateRequest().getFeatureTags());
+        DelegateRequest firstDelegateRequest = new DelegateRequest(firstDelegate);
+        SipDelegateController c1 = injectMockDelegateController(TEST_PACKAGE_NAME,
+                firstDelegateRequest);
+        createDelegateAndVerify(controller, c1, firstDelegateRequest, firstDelegate,
+                Collections.emptySet(), TEST_PACKAGE_NAME);
+
+        CompletableFuture<Integer> pendingDestroy =  setDestroyFuture(c1, true,
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SUBSCRIPTION_TORN_DOWN);
+        controller.onDestroy();
+        waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+        // verify change was called.
+        verify(c1).destroy(true /*force*/,
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SUBSCRIPTION_TORN_DOWN);
+        // ensure thread is not blocked while waiting for pending complete.
+        waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+        completePendingDestroy(pendingDestroy,
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SUBSCRIPTION_TORN_DOWN);
+    }
+
+    @SmallTest
+    @Test
+    public void testTimingSubIdChangedAndCreateNewSubId() throws Exception {
+        SipTransportController controller = setupLiveTransportController(THROTTLE_MS);
+
+        ArraySet<String> firstDelegate = new ArraySet<>(getBaseDelegateRequest().getFeatureTags());
+        DelegateRequest firstDelegateRequest = new DelegateRequest(firstDelegate);
+        SipDelegateController c1 = injectMockDelegateController(TEST_PACKAGE_NAME,
+                firstDelegateRequest);
+        CompletableFuture<Boolean> pendingC1Change = createDelegate(controller, c1,
+                firstDelegateRequest, firstDelegate, Collections.emptySet(), TEST_PACKAGE_NAME);
+        assertTrue(scheduleDelayedWait(2 * THROTTLE_MS));
+        verifyDelegateChanged(c1, pendingC1Change, firstDelegate, Collections.emptySet(), 0);
+
+
+        CompletableFuture<Integer> pendingDestroy =  setDestroyFuture(c1, true,
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SUBSCRIPTION_TORN_DOWN);
+        // triggers reeval now.
+        controller.onAssociatedSubscriptionUpdated(TEST_SUB_ID + 1);
+        waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+
+        // mock a second delegate with the new subId associated with the slot.
+        ArraySet<String> secondDelegate = new ArraySet<>();
+        secondDelegate.add(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG);
+        secondDelegate.add(ImsSignallingUtils.FILE_TRANSFER_HTTP_TAG);
+        DelegateRequest secondDelegateRequest = new DelegateRequest(secondDelegate);
+        SipDelegateController c2 = injectMockDelegateController(TEST_SUB_ID + 1,
+                TEST_PACKAGE_NAME, secondDelegateRequest);
+        CompletableFuture<Boolean> pendingC2Change = createDelegate(controller, c2,
+                TEST_SUB_ID + 1, secondDelegateRequest, secondDelegate,
+                Collections.emptySet(), TEST_PACKAGE_NAME);
+        assertTrue(scheduleDelayedWait(THROTTLE_MS));
+
+        //trigger destroyed event
+        verifyDestroyDelegate(controller, c1, pendingDestroy, true /*force*/,
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SUBSCRIPTION_TORN_DOWN);
+        assertTrue(scheduleDelayedWait(2 * THROTTLE_MS));
+        verifyDelegateChanged(c2, pendingC2Change, secondDelegate, Collections.emptySet(), 0);
+    }
+
+    @SafeVarargs
+    private final Pair<Set<String>, Set<FeatureTagState>> getAllowedAndDeniedTagsForConfig(
+            DelegateRequest r, int denyReason, Set<String>... previousRequestedTagSets) {
+        ArraySet<String> rejectedTags = new ArraySet<>(r.getFeatureTags());
+        ArraySet<String> grantedTags = new ArraySet<>(r.getFeatureTags());
+        Set<String> previousRequestedTags = new ArraySet<>();
+        for (Set<String> s : previousRequestedTagSets) {
+            previousRequestedTags.addAll(s);
+        }
+        rejectedTags.retainAll(previousRequestedTags);
+        grantedTags.removeAll(previousRequestedTags);
+        Set<FeatureTagState> deniedTags = getDeniedTagsForReason(rejectedTags, denyReason);
+        return new Pair<>(grantedTags, deniedTags);
+    }
+
+    private void completePendingChange(CompletableFuture<Boolean> change, boolean result) {
+        mExecutorService.execute(() -> change.complete(result));
+        waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+    }
+
+    private void completePendingDestroy(CompletableFuture<Integer> destroy, int result) {
+        mExecutorService.execute(() -> destroy.complete(result));
+        waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+    }
+
+    private SipTransportController setupLiveTransportController() throws Exception {
+        return setupLiveTransportController(0 /*throttleMs*/);
+    }
+
+    private SipTransportController setupLiveTransportController(int throttleMs) throws Exception {
+        mExecutorService = Executors.newSingleThreadScheduledExecutor();
+        SipTransportController controller = createControllerAndThrottle(mExecutorService,
+                throttleMs);
+        doReturn(mSipTransport).when(mRcsManager).getSipTransport();
+        controller.onAssociatedSubscriptionUpdated(TEST_SUB_ID);
+        controller.onRcsConnected(mRcsManager);
+        waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+        return controller;
+    }
+
+    private void createDelegateAndVerify(SipTransportController controller,
+            SipDelegateController delegateController, DelegateRequest r, Set<String> allowedTags,
+            Set<FeatureTagState> deniedTags, String packageName,
+            int numPreviousChanges) throws ImsException {
+
+        CompletableFuture<Boolean> pendingChange = createDelegate(controller, delegateController, r,
+                allowedTags, deniedTags, packageName);
+        verifyDelegateChanged(delegateController, pendingChange, allowedTags, deniedTags,
+                numPreviousChanges);
+    }
+
+    private void createDelegateAndVerify(SipTransportController controller,
+            SipDelegateController delegateController, DelegateRequest r, Set<String> allowedTags,
+            Set<FeatureTagState> deniedTags, String packageName) throws ImsException {
+        createDelegateAndVerify(controller, delegateController, r, allowedTags, deniedTags,
+                packageName, 0);
+    }
+
+    private CompletableFuture<Boolean> createDelegate(SipTransportController controller,
+            SipDelegateController delegateController, int subId, DelegateRequest r,
+            Set<String> allowedTags, Set<FeatureTagState> deniedTags, String packageName) {
+        CompletableFuture<Boolean> pendingChange = setChangeSupportedFeatureTagsFuture(
+                delegateController, allowedTags, deniedTags);
+        try {
+            controller.createSipDelegate(subId, r, packageName, mMockStateCallback,
+                    mMockMessageCallback);
+        } catch (ImsException e) {
+            fail("ImsException thrown:" + e);
+        }
+        // move to internal & schedule eval
+        waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+        // reeval
+        waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+        return pendingChange;
+    }
+
+    private CompletableFuture<Boolean> createDelegate(SipTransportController controller,
+            SipDelegateController delegateController, DelegateRequest r, Set<String> allowedTags,
+            Set<FeatureTagState> deniedTags, String packageName) throws ImsException {
+        return createDelegate(controller, delegateController, TEST_SUB_ID, r, allowedTags,
+                deniedTags, packageName);
+    }
+
+    private void verifyDelegateChanged(SipDelegateController delegateController,
+            CompletableFuture<Boolean> pendingChange, Set<String> allowedTags,
+            Set<FeatureTagState> deniedTags, int numPreviousChangeStages) {
+        // empty the queue of pending changeSupportedFeatureTags before running the one we are
+        // interested in, since the reevaluate waits for one stage to complete before moving to the
+        // next.
+        for (int i = 0; i < numPreviousChangeStages + 1; i++) {
+            assertTrue(waitForExecutorAction(mExecutorService, TIMEOUT_MS));
+        }
+        // verify change was called.
+        verify(delegateController).changeSupportedFeatureTags(allowedTags, deniedTags);
+        // ensure thread is not blocked while waiting for pending complete.
+        assertTrue(waitForExecutorAction(mExecutorService, TIMEOUT_MS));
+        completePendingChange(pendingChange, true);
+        // process pending change.
+        assertTrue(waitForExecutorAction(mExecutorService, TIMEOUT_MS));
+    }
+
+    private void destroyDelegateAndVerify(SipTransportController controller,
+            SipDelegateController delegateController, boolean force, int reason) {
+        CompletableFuture<Integer> pendingDestroy =  destroyDelegate(controller, delegateController,
+                force, reason);
+        verifyDestroyDelegate(controller, delegateController, pendingDestroy, force, reason);
+    }
+
+    private CompletableFuture<Integer> destroyDelegate(SipTransportController controller,
+            SipDelegateController delegateController, boolean force, int reason) {
+        CompletableFuture<Integer> pendingDestroy =  setDestroyFuture(delegateController, force,
+                reason);
+        controller.destroySipDelegate(TEST_SUB_ID, delegateController.getSipDelegateInterface(),
+                reason);
+        // move to internal & schedule eval
+        waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+        // reeval
+        waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+        return pendingDestroy;
+    }
+
+    private void verifyDestroyDelegate(SipTransportController controller,
+            SipDelegateController delegateController, CompletableFuture<Integer> pendingDestroy,
+            boolean force, int reason) {
+        // verify destroy was called.
+        verify(delegateController).destroy(force, reason);
+        // ensure thread is not blocked while waiting for pending complete.
+        waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+        completePendingDestroy(pendingDestroy, reason);
+    }
+
+    private DelegateRequest getBaseDelegateRequest() {
+        Set<String> featureTags = new ArraySet<>();
+        featureTags.add(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG);
+        featureTags.add(ImsSignallingUtils.GROUP_CHAT_TAG);
+        featureTags.add(ImsSignallingUtils.FILE_TRANSFER_HTTP_TAG);
+        return new DelegateRequest(featureTags);
+    }
+
+    private Set<FeatureTagState> getBaseDeniedSet() {
+        Set<FeatureTagState> deniedTags = new ArraySet<>();
+        deniedTags.add(new FeatureTagState(ImsSignallingUtils.MMTEL_TAG,
+                SipDelegateManager.DENIED_REASON_IN_USE_BY_ANOTHER_DELEGATE));
+        return deniedTags;
+    }
+
+    private Set<FeatureTagState> getDeniedTagsForReason(Set<String> deniedTags, int reason) {
+        return deniedTags.stream().map(t -> new FeatureTagState(t, reason))
+                .collect(Collectors.toSet());
+    }
+
+    private SipDelegateController injectMockDelegateController(String packageName,
+            DelegateRequest r) {
+        return injectMockDelegateController(TEST_SUB_ID, packageName, r);
+    }
+
+    private SipDelegateController injectMockDelegateController(int subId, String packageName,
+            DelegateRequest r) {
+        SipDelegateControllerContainer c = new SipDelegateControllerContainer(subId,
+                packageName, r);
+        mMockControllers.add(c);
+        return c.delegateController;
+    }
+
+    private SipDelegateController getMockDelegateController(int subId, String packageName,
+            DelegateRequest r) {
+        return mMockControllers.stream()
+                .filter(c -> c.subId == subId && c.packageName.equals(packageName)
+                        && c.delegateRequest.equals(r))
+                .map(c -> c.delegateController).findFirst().orElse(null);
+    }
+
+    private CompletableFuture<Boolean> setChangeSupportedFeatureTagsFuture(SipDelegateController c,
+            Set<String> supportedSet, Set<FeatureTagState> deniedSet) {
+        CompletableFuture<Boolean> result = new CompletableFuture<>();
+        doReturn(result).when(c).changeSupportedFeatureTags(eq(supportedSet), eq(deniedSet));
+        return result;
+    }
+
+    private CompletableFuture<Integer> setDestroyFuture(SipDelegateController c, boolean force,
+            int destroyReason) {
+        CompletableFuture<Integer> result = new CompletableFuture<>();
+        doReturn(result).when(c).destroy(force, destroyReason);
+        return result;
+    }
+
+    private void setSmsRoleAndEvaluate(SipTransportController c, String packageName) {
+        verify(mMockRoleManager).addOnRoleHoldersChangedListenerAsUser(any(), any(), any());
+        mSmsPackageName.clear();
+        mSmsPackageName.add(packageName);
+        c.onRoleHoldersChanged(RoleManager.ROLE_SMS, UserHandle.SYSTEM);
+        // finish internal throttled re-evaluate
+        waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+    }
+
+    private SipTransportController createController(ScheduledExecutorService e) {
+        return createControllerAndThrottle(e, 0 /*throttleMs*/);
+    }
+
+    private SipTransportController createControllerAndThrottle(ScheduledExecutorService e,
+            int throttleMs) {
+        return new SipTransportController(mContext, 0 /*slotId*/, TEST_SUB_ID,
+                mMockDelegateControllerFactory, mMockRoleManager,
+                // Remove delays for testing.
+                new SipTransportController.TimerAdapter() {
+                    @Override
+                    public int getReevaluateThrottleTimerMilliseconds() {
+                        return throttleMs;
+                    }
+
+                    @Override
+                    public int getUpdateRegistrationDelayMilliseconds() {
+                        return 0;
+                    }
+                }, e);
+    }
+
+    private boolean scheduleDelayedWait(long timeMs) {
+        CountDownLatch l = new CountDownLatch(1);
+        mExecutorService.schedule(l::countDown, timeMs, TimeUnit.MILLISECONDS);
+        while (l.getCount() > 0) {
+            try {
+                return l.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+            } catch (InterruptedException e) {
+                // try again
+            }
+        }
+        return true;
     }
 }