Scaffolding for SipTransport Impl & Implement SipTransport#isSupported

Creates a new SipTransportController class, which hooks into
TelephonyRcsService and will implement the SipDelegate
management.

SipDelegateManager#isSupported is implemented, which
ensures all of the following are true before returning
supported:
- ImsService#CAPABILITY_SIP_DELEGATE_CREATION is set on ImsService
- ImsService returns non-null implementation of SipTransportImplBase
- CarrierConfigManager.Ims.KEY_RCS_SINGLE_REGISTRATION_REQUIRED_BOOL is true

Bug: 154763999
Test: atest TeleServiceTests
Merged-In: I01677351c53b8c8480736f1a6539ece0bedf685f
Change-Id: I01677351c53b8c8480736f1a6539ece0bedf685f
diff --git a/tests/src/com/android/TestContext.java b/tests/src/com/android/TestContext.java
index 13bfe3b..9d712d3 100644
--- a/tests/src/com/android/TestContext.java
+++ b/tests/src/com/android/TestContext.java
@@ -17,7 +17,7 @@
 package com.android;
 
 import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doAnswer;
 
 import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
@@ -32,9 +32,11 @@
 import android.telephony.TelephonyManager;
 import android.telephony.ims.ImsManager;
 import android.test.mock.MockContext;
+import android.util.SparseArray;
 
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
+import org.mockito.stubbing.Answer;
 
 import java.util.concurrent.Executor;
 
@@ -46,11 +48,19 @@
     @Mock SubscriptionManager mMockSubscriptionManager;
     @Mock ImsManager mMockImsManager;
 
-    private PersistableBundle mCarrierConfig = new PersistableBundle();
+    private SparseArray<PersistableBundle> mCarrierConfigs = new SparseArray<>();
 
     public TestContext() {
         MockitoAnnotations.initMocks(this);
-        doReturn(mCarrierConfig).when(mMockCarrierConfigManager).getConfigForSubId(anyInt());
+        doAnswer((Answer<PersistableBundle>) invocation -> {
+            int subId = (int) invocation.getArguments()[0];
+            if (subId < 0) {
+                return new PersistableBundle();
+            }
+            PersistableBundle b = mCarrierConfigs.get(subId);
+
+            return (b != null ? b : new PersistableBundle());
+        }).when(mMockCarrierConfigManager).getConfigForSubId(anyInt());
     }
 
     @Override
@@ -140,7 +150,15 @@
         return null;
     }
 
-    public PersistableBundle getCarrierConfig() {
-        return mCarrierConfig;
+    /**
+     * @return CarrierConfig PersistableBundle for the subscription specified.
+     */
+    public PersistableBundle getCarrierConfig(int subId) {
+        PersistableBundle b = mCarrierConfigs.get(subId);
+        if (b == null) {
+            b = new PersistableBundle();
+            mCarrierConfigs.put(subId, b);
+        }
+        return b;
     }
 }
diff --git a/tests/src/com/android/TestExecutorService.java b/tests/src/com/android/TestExecutorService.java
new file mode 100644
index 0000000..fec502a
--- /dev/null
+++ b/tests/src/com/android/TestExecutorService.java
@@ -0,0 +1,193 @@
+/*
+ * 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;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Delayed;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * An implementation of ExecutorService that just runs the requested task on the thread that it
+ * was called on for testing purposes.
+ */
+public class TestExecutorService implements ScheduledExecutorService {
+
+    private static class CompletedFuture<T> implements Future<T>, ScheduledFuture<T> {
+
+        private final Callable<T> mTask;
+        private final long mDelayMs;
+
+        CompletedFuture(Callable<T> task) {
+            mTask = task;
+            mDelayMs = 0;
+        }
+
+        CompletedFuture(Callable<T> task, long delayMs) {
+            mTask = task;
+            mDelayMs = delayMs;
+        }
+
+        @Override
+        public boolean cancel(boolean mayInterruptIfRunning) {
+            return false;
+        }
+
+        @Override
+        public boolean isCancelled() {
+            return false;
+        }
+
+        @Override
+        public boolean isDone() {
+            return true;
+        }
+
+        @Override
+        public T get() throws InterruptedException, ExecutionException {
+            try {
+                return mTask.call();
+            } catch (Exception e) {
+                throw new ExecutionException(e);
+            }
+        }
+
+        @Override
+        public T get(long timeout, TimeUnit unit)
+                throws InterruptedException, ExecutionException, TimeoutException {
+            try {
+                return mTask.call();
+            } catch (Exception e) {
+                throw new ExecutionException(e);
+            }
+        }
+
+        @Override
+        public long getDelay(TimeUnit unit) {
+            if (unit == TimeUnit.MILLISECONDS) {
+                return mDelayMs;
+            } else {
+                // not implemented
+                return 0;
+            }
+        }
+
+        @Override
+        public int compareTo(Delayed o) {
+            if (o == null) return 1;
+            if (o.getDelay(TimeUnit.MILLISECONDS) > mDelayMs) return -1;
+            if (o.getDelay(TimeUnit.MILLISECONDS) < mDelayMs) return 1;
+            return 0;
+        }
+    }
+
+    @Override
+    public void shutdown() {
+    }
+
+    @Override
+    public List<Runnable> shutdownNow() {
+        return null;
+    }
+
+    @Override
+    public boolean isShutdown() {
+        return false;
+    }
+
+    @Override
+    public boolean isTerminated() {
+        return false;
+    }
+
+    @Override
+    public boolean awaitTermination(long timeout, TimeUnit unit) {
+        return false;
+    }
+
+    @Override
+    public <T> Future<T> submit(Callable<T> task) {
+        return new CompletedFuture<>(task);
+    }
+
+    @Override
+    public <T> Future<T> submit(Runnable task, T result) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    @Override
+    public Future<?> submit(Runnable task) {
+        task.run();
+        return new CompletedFuture<>(() -> null);
+    }
+
+    @Override
+    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    @Override
+    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout,
+            TimeUnit unit) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    @Override
+    public <T> T invokeAny(Collection<? extends Callable<T>> tasks) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    @Override
+    public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    @Override
+    public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
+        // No need to worry about delays yet
+        command.run();
+        return new CompletedFuture<>(() -> null, delay);
+    }
+
+    @Override
+    public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {
+        return new CompletedFuture<>(callable, delay);
+    }
+
+    @Override
+    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period,
+            TimeUnit unit) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    @Override
+    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay,
+            long delay, TimeUnit unit) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    @Override
+    public void execute(Runnable command) {
+        command.run();
+    }
+}
diff --git a/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java b/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
index 2060e6f..07fe6a8 100644
--- a/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
+++ b/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
@@ -936,14 +936,14 @@
 
         // Setup test to not support SUPL on the non-DDS subscription
         doReturn(true).when(mDeviceState).isSuplDdsSwitchRequiredForEmergencyCall(any());
-        getTestContext().getCarrierConfig().putStringArray(
+        getTestContext().getCarrierConfig(0 /*subId*/).putStringArray(
                 CarrierConfigManager.Gps.KEY_ES_SUPL_DATA_PLANE_ONLY_ROAMING_PLMN_STRING_ARRAY,
                 null);
         testPhone.getServiceState().setRoaming(false);
-        getTestContext().getCarrierConfig().putInt(
+        getTestContext().getCarrierConfig(0 /*subId*/).putInt(
                 CarrierConfigManager.Gps.KEY_ES_SUPL_CONTROL_PLANE_SUPPORT_INT,
                 CarrierConfigManager.Gps.SUPL_EMERGENCY_MODE_TYPE_DP_ONLY);
-        getTestContext().getCarrierConfig().putString(
+        getTestContext().getCarrierConfig(0 /*subId*/).putString(
                 CarrierConfigManager.Gps.KEY_ES_EXTENSION_SEC_STRING, "150");
         delayDialRunnable.run();
 
@@ -1021,14 +1021,14 @@
 
         // Setup test to not support SUPL on the non-DDS subscription
         doReturn(true).when(mDeviceState).isSuplDdsSwitchRequiredForEmergencyCall(any());
-        getTestContext().getCarrierConfig().putStringArray(
+        getTestContext().getCarrierConfig(0 /*subId*/).putStringArray(
                 CarrierConfigManager.Gps.KEY_ES_SUPL_DATA_PLANE_ONLY_ROAMING_PLMN_STRING_ARRAY,
                 null);
         testPhone.getServiceState().setRoaming(false);
-        getTestContext().getCarrierConfig().putInt(
+        getTestContext().getCarrierConfig(0 /*subId*/).putInt(
                 CarrierConfigManager.Gps.KEY_ES_SUPL_CONTROL_PLANE_SUPPORT_INT,
                 CarrierConfigManager.Gps.SUPL_EMERGENCY_MODE_TYPE_CP_FALLBACK);
-        getTestContext().getCarrierConfig().putString(
+        getTestContext().getCarrierConfig(0 /*subId*/).putString(
                 CarrierConfigManager.Gps.KEY_ES_EXTENSION_SEC_STRING, "0");
         delayDialRunnable.run();
 
@@ -1047,14 +1047,14 @@
 
         // If the non-DDS supports SUPL, dont switch data
         doReturn(false).when(mDeviceState).isSuplDdsSwitchRequiredForEmergencyCall(any());
-        getTestContext().getCarrierConfig().putStringArray(
+        getTestContext().getCarrierConfig(0 /*subId*/).putStringArray(
                 CarrierConfigManager.Gps.KEY_ES_SUPL_DATA_PLANE_ONLY_ROAMING_PLMN_STRING_ARRAY,
                 null);
         testPhone.getServiceState().setRoaming(false);
-        getTestContext().getCarrierConfig().putInt(
+        getTestContext().getCarrierConfig(0 /*subId*/).putInt(
                 CarrierConfigManager.Gps.KEY_ES_SUPL_CONTROL_PLANE_SUPPORT_INT,
                 CarrierConfigManager.Gps.SUPL_EMERGENCY_MODE_TYPE_DP_ONLY);
-        getTestContext().getCarrierConfig().putString(
+        getTestContext().getCarrierConfig(0 /*subId*/).putString(
                 CarrierConfigManager.Gps.KEY_ES_EXTENSION_SEC_STRING, "0");
         delayDialRunnable.run();
 
@@ -1073,14 +1073,14 @@
 
         // Setup test to not support SUPL on the non-DDS subscription
         doReturn(true).when(mDeviceState).isSuplDdsSwitchRequiredForEmergencyCall(any());
-        getTestContext().getCarrierConfig().putStringArray(
+        getTestContext().getCarrierConfig(0 /*subId*/).putStringArray(
                 CarrierConfigManager.Gps.KEY_ES_SUPL_DATA_PLANE_ONLY_ROAMING_PLMN_STRING_ARRAY,
                 null);
         testPhone.getServiceState().setRoaming(true);
-        getTestContext().getCarrierConfig().putInt(
+        getTestContext().getCarrierConfig(0 /*subId*/).putInt(
                 CarrierConfigManager.Gps.KEY_ES_SUPL_CONTROL_PLANE_SUPPORT_INT,
                 CarrierConfigManager.Gps.SUPL_EMERGENCY_MODE_TYPE_DP_ONLY);
-        getTestContext().getCarrierConfig().putString(
+        getTestContext().getCarrierConfig(0 /*subId*/).putString(
                 CarrierConfigManager.Gps.KEY_ES_EXTENSION_SEC_STRING, "0");
         delayDialRunnable.run();
 
@@ -1107,13 +1107,13 @@
         doReturn(true).when(mDeviceState).isSuplDdsSwitchRequiredForEmergencyCall(any());
         String[] roamingPlmns = new String[1];
         roamingPlmns[0] = testRoamingOperator;
-        getTestContext().getCarrierConfig().putStringArray(
+        getTestContext().getCarrierConfig(0 /*subId*/).putStringArray(
                 CarrierConfigManager.Gps.KEY_ES_SUPL_DATA_PLANE_ONLY_ROAMING_PLMN_STRING_ARRAY,
                 roamingPlmns);
-        getTestContext().getCarrierConfig().putInt(
+        getTestContext().getCarrierConfig(0 /*subId*/).putInt(
                 CarrierConfigManager.Gps.KEY_ES_SUPL_CONTROL_PLANE_SUPPORT_INT,
                 CarrierConfigManager.Gps.SUPL_EMERGENCY_MODE_TYPE_CP_FALLBACK);
-        getTestContext().getCarrierConfig().putString(
+        getTestContext().getCarrierConfig(0 /*subId*/).putString(
                 CarrierConfigManager.Gps.KEY_ES_EXTENSION_SEC_STRING, "0");
         delayDialRunnable.run();
 
@@ -1140,13 +1140,13 @@
         doReturn(true).when(mDeviceState).isSuplDdsSwitchRequiredForEmergencyCall(any());
         String[] roamingPlmns = new String[1];
         roamingPlmns[0] = testRoamingOperator;
-        getTestContext().getCarrierConfig().putStringArray(
+        getTestContext().getCarrierConfig(0 /*subId*/).putStringArray(
                 CarrierConfigManager.Gps.KEY_ES_SUPL_DATA_PLANE_ONLY_ROAMING_PLMN_STRING_ARRAY,
                 roamingPlmns);
-        getTestContext().getCarrierConfig().putInt(
+        getTestContext().getCarrierConfig(0 /*subId*/).putInt(
                 CarrierConfigManager.Gps.KEY_ES_SUPL_CONTROL_PLANE_SUPPORT_INT,
                 CarrierConfigManager.Gps.SUPL_EMERGENCY_MODE_TYPE_CP_FALLBACK);
-        getTestContext().getCarrierConfig().putString(
+        getTestContext().getCarrierConfig(0 /*subId*/).putString(
                 CarrierConfigManager.Gps.KEY_ES_EXTENSION_SEC_STRING, "0");
         delayDialRunnable.run();
 
diff --git a/tests/src/com/android/services/telephony/rcs/SipTransportControllerTest.java b/tests/src/com/android/services/telephony/rcs/SipTransportControllerTest.java
new file mode 100644
index 0000000..65a95cd
--- /dev/null
+++ b/tests/src/com/android/services/telephony/rcs/SipTransportControllerTest.java
@@ -0,0 +1,156 @@
+/*
+ * 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.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+
+import android.telephony.ims.ImsException;
+import android.telephony.ims.aidl.ISipTransport;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.TelephonyTestBase;
+import com.android.TestExecutorService;
+import com.android.ims.RcsFeatureManager;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class SipTransportControllerTest extends TelephonyTestBase {
+
+    @Mock private RcsFeatureManager mRcsManager;
+    @Mock private ISipTransport mSipTransport;
+
+    private final TestExecutorService mExecutorService = new TestExecutorService();
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @SmallTest
+    @Test
+    public void isSupportedRcsNotConnected() {
+        SipTransportController controller = createController(0 /*slotId*/, 0 /*subId*/);
+        try {
+            controller.isSupported(0 /*subId*/);
+            fail();
+        } catch (ImsException e) {
+            assertEquals(ImsException.CODE_ERROR_SERVICE_UNAVAILABLE, e.getCode());
+        }
+    }
+
+    @SmallTest
+    @Test
+    public void isSupportedInvalidSubId() {
+        SipTransportController controller = createController(0 /*slotId*/, 0 /*subId*/);
+        try {
+            controller.isSupported(1 /*subId*/);
+            fail();
+        } catch (ImsException e) {
+            assertEquals(ImsException.CODE_ERROR_INVALID_SUBSCRIPTION, e.getCode());
+        }
+    }
+
+    @SmallTest
+    @Test
+    public void isSupportedSubIdChanged() {
+        SipTransportController controller = createController(0 /*slotId*/, 0 /*subId*/);
+        controller.onAssociatedSubscriptionUpdated(1 /*subId*/);
+        try {
+            controller.isSupported(0 /*subId*/);
+            fail();
+        } catch (ImsException e) {
+            assertEquals(ImsException.CODE_ERROR_INVALID_SUBSCRIPTION, e.getCode());
+        }
+    }
+
+    @SmallTest
+    @Test
+    public void isSupportedSipTransportAvailableRcsConnected() throws Exception {
+        SipTransportController controller = createController(0 /*slotId*/, 0 /*subId*/);
+        doReturn(mSipTransport).when(mRcsManager).getSipTransport();
+        controller.onRcsConnected(mRcsManager);
+        try {
+            assertTrue(controller.isSupported(0 /*subId*/));
+        } catch (ImsException e) {
+            fail();
+        }
+    }
+
+    @SmallTest
+    @Test
+    public void isSupportedSipTransportNotAvailableRcsDisconnected() throws Exception {
+        SipTransportController controller = createController(0 /*slotId*/, 0 /*subId*/);
+        doReturn(mSipTransport).when(mRcsManager).getSipTransport();
+        controller.onRcsConnected(mRcsManager);
+        controller.onRcsDisconnected();
+        try {
+            controller.isSupported(0 /*subId*/);
+            fail();
+        } catch (ImsException e) {
+            assertEquals(ImsException.CODE_ERROR_SERVICE_UNAVAILABLE, e.getCode());
+        }
+    }
+
+    @SmallTest
+    @Test
+    public void isSupportedSipTransportNotAvailableRcsConnected() throws Exception {
+        SipTransportController controller = createController(0 /*slotId*/, 0 /*subId*/);
+        doReturn(null).when(mRcsManager).getSipTransport();
+        controller.onRcsConnected(mRcsManager);
+        try {
+            assertFalse(controller.isSupported(0 /*subId*/));
+        } catch (ImsException e) {
+            fail();
+        }
+    }
+
+    @SmallTest
+    @Test
+    public void isSupportedImsServiceNotAvailableRcsConnected() throws Exception {
+        SipTransportController controller = createController(0 /*slotId*/, 0 /*subId*/);
+        doThrow(new ImsException("", ImsException.CODE_ERROR_SERVICE_UNAVAILABLE))
+                .when(mRcsManager).getSipTransport();
+        controller.onRcsConnected(mRcsManager);
+        try {
+            controller.isSupported(0 /*subId*/);
+            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);
+    }
+}
diff --git a/tests/src/com/android/services/telephony/rcs/TelephonyRcsServiceTest.java b/tests/src/com/android/services/telephony/rcs/TelephonyRcsServiceTest.java
index cfb68b7..ffbb71d 100644
--- a/tests/src/com/android/services/telephony/rcs/TelephonyRcsServiceTest.java
+++ b/tests/src/com/android/services/telephony/rcs/TelephonyRcsServiceTest.java
@@ -52,6 +52,8 @@
     @Mock TelephonyRcsService.FeatureFactory mFeatureFactory;
     @Mock UserCapabilityExchangeImpl mMockUceSlot0;
     @Mock UserCapabilityExchangeImpl mMockUceSlot1;
+    @Mock SipTransportController mMockSipTransportSlot0;
+    @Mock SipTransportController mMockSipTransportSlot1;
     @Mock RcsFeatureController.RegistrationHelperFactory mRegistrationFactory;
     @Mock RcsFeatureController.FeatureConnectorFactory<RcsFeatureManager> mFeatureConnectorFactory;
     @Mock FeatureConnector<RcsFeatureManager> mFeatureConnector;
@@ -72,6 +74,10 @@
                 anyInt());
         doReturn(mMockUceSlot1).when(mFeatureFactory).createUserCapabilityExchange(any(), eq(1),
                 anyInt());
+        doReturn(mMockSipTransportSlot0).when(mFeatureFactory).createSipTransportController(any(),
+                eq(0), anyInt());
+        doReturn(mMockSipTransportSlot1).when(mFeatureFactory).createSipTransportController(any(),
+                eq(1), anyInt());
         //set up default slot-> sub ID mappings.
         setSlotToSubIdMapping(0 /*slotId*/, 1/*subId*/);
         setSlotToSubIdMapping(1 /*slotId*/, 2/*subId*/);
@@ -84,7 +90,8 @@
 
     @Test
     public void testUserCapabilityExchangePresenceConnected() {
-        setCarrierConfig(CarrierConfigManager.KEY_USE_RCS_PRESENCE_BOOL, true /*isEnabled*/);
+        setCarrierConfig(1 /*subId*/, CarrierConfigManager.KEY_USE_RCS_PRESENCE_BOOL,
+                true /*isEnabled*/);
         createRcsService(1 /*numSlots*/);
         verify(mFeatureControllerSlot0).addFeature(mMockUceSlot0, UserCapabilityExchangeImpl.class);
         verify(mFeatureControllerSlot0).connect();
@@ -92,7 +99,8 @@
 
     @Test
     public void testUserCapabilityExchangeOptionsConnected() {
-        setCarrierConfig(CarrierConfigManager.KEY_USE_RCS_SIP_OPTIONS_BOOL, true /*isEnabled*/);
+        setCarrierConfig(1 /*subId*/, CarrierConfigManager.KEY_USE_RCS_SIP_OPTIONS_BOOL,
+                true /*isEnabled*/);
         createRcsService(1 /*numSlots*/);
         verify(mFeatureControllerSlot0).addFeature(mMockUceSlot0, UserCapabilityExchangeImpl.class);
         verify(mFeatureControllerSlot0).connect();
@@ -108,6 +116,55 @@
     }
 
     @Test
+    public void testSipTransportConnected() {
+        createRcsService(1 /*numSlots*/);
+        verify(mFeatureControllerSlot0, never()).addFeature(mMockSipTransportSlot0,
+                SipTransportController.class);
+        verify(mFeatureControllerSlot0, never()).connect();
+
+
+        // Send carrier config update for each slot.
+        setCarrierConfig(1 /*subId*/,
+                CarrierConfigManager.Ims.KEY_IMS_SINGLE_REGISTRATION_REQUIRED_BOOL,
+                true /*isEnabled*/);
+        sendCarrierConfigChanged(0 /*slotId*/, 1 /*subId*/);
+        verify(mFeatureControllerSlot0).addFeature(mMockSipTransportSlot0,
+                SipTransportController.class);
+        verify(mFeatureControllerSlot0).connect();
+        verify(mFeatureControllerSlot0).updateAssociatedSubscription(1);
+    }
+
+    @Test
+    public void testSipTransportConnectedOneSlot() {
+        createRcsService(2 /*numSlots*/);
+        verify(mFeatureControllerSlot0, never()).addFeature(mMockSipTransportSlot0,
+                SipTransportController.class);
+        verify(mFeatureControllerSlot0, never()).connect();
+        verify(mFeatureControllerSlot0, never()).addFeature(mMockSipTransportSlot1,
+                SipTransportController.class);
+        verify(mFeatureControllerSlot1, never()).connect();
+
+
+        // Send carrier config update for slot 0 only
+        setCarrierConfig(1 /*subId*/,
+                CarrierConfigManager.Ims.KEY_IMS_SINGLE_REGISTRATION_REQUIRED_BOOL,
+                true /*isEnabled*/);
+        setCarrierConfig(2 /*subId*/,
+                CarrierConfigManager.Ims.KEY_IMS_SINGLE_REGISTRATION_REQUIRED_BOOL,
+                false /*isEnabled*/);
+        sendCarrierConfigChanged(0 /*slotId*/, 1 /*subId*/);
+        sendCarrierConfigChanged(1 /*slotId*/, 2 /*subId*/);
+        verify(mFeatureControllerSlot0).addFeature(mMockSipTransportSlot0,
+                SipTransportController.class);
+        verify(mFeatureControllerSlot1, never()).addFeature(mMockSipTransportSlot0,
+                SipTransportController.class);
+        verify(mFeatureControllerSlot0).connect();
+        verify(mFeatureControllerSlot1, never()).connect();
+        verify(mFeatureControllerSlot0).updateAssociatedSubscription(1);
+        verify(mFeatureControllerSlot1, never()).updateAssociatedSubscription(1);
+    }
+
+    @Test
     public void testNoFeaturesEnabledCarrierConfigChanged() {
         createRcsService(1 /*numSlots*/);
         // No carrier config set for UCE.
@@ -122,7 +179,10 @@
 
     @Test
     public void testSlotUpdates() {
-        setCarrierConfig(CarrierConfigManager.KEY_USE_RCS_PRESENCE_BOOL, true /*isEnabled*/);
+        setCarrierConfig(1 /*subId*/, CarrierConfigManager.KEY_USE_RCS_PRESENCE_BOOL,
+                true /*isEnabled*/);
+        setCarrierConfig(2 /*subId*/, CarrierConfigManager.KEY_USE_RCS_PRESENCE_BOOL,
+                true /*isEnabled*/);
         TelephonyRcsService service = createRcsService(1 /*numSlots*/);
         verify(mFeatureControllerSlot0).addFeature(mMockUceSlot0, UserCapabilityExchangeImpl.class);
         verify(mFeatureControllerSlot0).connect();
@@ -163,7 +223,10 @@
 
     @Test
     public void testCarrierConfigUpdate() {
-        setCarrierConfig(CarrierConfigManager.KEY_USE_RCS_PRESENCE_BOOL, true /*isEnabled*/);
+        setCarrierConfig(1 /*subId*/, CarrierConfigManager.KEY_USE_RCS_PRESENCE_BOOL,
+                true /*isEnabled*/);
+        setCarrierConfig(2 /*subId*/, CarrierConfigManager.KEY_USE_RCS_PRESENCE_BOOL,
+                true /*isEnabled*/);
         createRcsService(2 /*numSlots*/);
         verify(mFeatureControllerSlot0).addFeature(mMockUceSlot0, UserCapabilityExchangeImpl.class);
         verify(mFeatureControllerSlot1).addFeature(mMockUceSlot1, UserCapabilityExchangeImpl.class);
@@ -182,20 +245,42 @@
 
     @Test
     public void testCarrierConfigUpdateUceToNoUce() {
-        setCarrierConfig(CarrierConfigManager.KEY_USE_RCS_PRESENCE_BOOL, true /*isEnabled*/);
+        setCarrierConfig(1 /*subId*/, CarrierConfigManager.KEY_USE_RCS_PRESENCE_BOOL,
+                true /*isEnabled*/);
         createRcsService(1 /*numSlots*/);
         verify(mFeatureControllerSlot0).addFeature(mMockUceSlot0, UserCapabilityExchangeImpl.class);
         verify(mFeatureControllerSlot0).connect();
 
 
         // Send carrier config update for each slot.
-        setCarrierConfig(CarrierConfigManager.KEY_USE_RCS_PRESENCE_BOOL, false /*isEnabled*/);
+        setCarrierConfig(1 /*subId*/, CarrierConfigManager.KEY_USE_RCS_PRESENCE_BOOL,
+                false /*isEnabled*/);
         sendCarrierConfigChanged(0 /*slotId*/, 1 /*subId*/);
         verify(mFeatureControllerSlot0).removeFeature(UserCapabilityExchangeImpl.class);
         verify(mFeatureControllerSlot0).updateAssociatedSubscription(1);
     }
 
     @Test
+    public void testCarrierConfigUpdateTransportToNoTransport() {
+        setCarrierConfig(1 /*subId*/,
+                CarrierConfigManager.Ims.KEY_IMS_SINGLE_REGISTRATION_REQUIRED_BOOL,
+                true /*isEnabled*/);
+        createRcsService(1 /*numSlots*/);
+        verify(mFeatureControllerSlot0).addFeature(mMockSipTransportSlot0,
+                SipTransportController.class);
+        verify(mFeatureControllerSlot0).connect();
+
+
+        // Send carrier config update for each slot.
+        setCarrierConfig(1 /*subId*/,
+                CarrierConfigManager.Ims.KEY_IMS_SINGLE_REGISTRATION_REQUIRED_BOOL,
+                false /*isEnabled*/);
+        sendCarrierConfigChanged(0 /*slotId*/, 1 /*subId*/);
+        verify(mFeatureControllerSlot0).removeFeature(SipTransportController.class);
+        verify(mFeatureControllerSlot0).updateAssociatedSubscription(1);
+    }
+
+    @Test
     public void testCarrierConfigUpdateNoUceToUce() {
         createRcsService(1 /*numSlots*/);
         verify(mFeatureControllerSlot0, never()).addFeature(mMockUceSlot0,
@@ -204,7 +289,8 @@
 
 
         // Send carrier config update for each slot.
-        setCarrierConfig(CarrierConfigManager.KEY_USE_RCS_PRESENCE_BOOL, true /*isEnabled*/);
+        setCarrierConfig(1 /*subId*/, CarrierConfigManager.KEY_USE_RCS_PRESENCE_BOOL,
+                true /*isEnabled*/);
         sendCarrierConfigChanged(0 /*slotId*/, 1 /*subId*/);
         verify(mFeatureControllerSlot0).addFeature(mMockUceSlot0, UserCapabilityExchangeImpl.class);
         verify(mFeatureControllerSlot0).connect();
@@ -218,8 +304,8 @@
         mReceiverCaptor.getValue().onReceive(mContext, intent);
     }
 
-    private void setCarrierConfig(String key, boolean value) {
-        PersistableBundle bundle = mContext.getCarrierConfig();
+    private void setCarrierConfig(int subId, String key, boolean value) {
+        PersistableBundle bundle = mContext.getCarrierConfig(subId);
         bundle.putBoolean(key, value);
     }