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/src/com/android/phone/ImsRcsController.java b/src/com/android/phone/ImsRcsController.java
index 122386d..fd61936 100644
--- a/src/com/android/phone/ImsRcsController.java
+++ b/src/com/android/phone/ImsRcsController.java
@@ -42,6 +42,7 @@
 import com.android.internal.telephony.ims.ImsResolver;
 import com.android.internal.telephony.imsphone.ImsPhone;
 import com.android.services.telephony.rcs.RcsFeatureController;
+import com.android.services.telephony.rcs.SipTransportController;
 import com.android.services.telephony.rcs.TelephonyRcsService;
 import com.android.services.telephony.rcs.UserCapabilityExchangeImpl;
 
@@ -353,6 +354,29 @@
         }
     }
 
+    @Override
+    public boolean isSipDelegateSupported(int subId) {
+        enforceReadPrivilegedPermission("isSipDelegateSupported");
+        final long token = Binder.clearCallingIdentity();
+        try {
+            SipTransportController transport = getRcsFeatureController(subId).getFeature(
+                    SipTransportController.class);
+            if (transport == null) {
+                return false;
+            }
+            return transport.isSupported(subId);
+        } catch (ImsException e) {
+            throw new ServiceSpecificException(e.getCode(), e.getMessage());
+        } catch (ServiceSpecificException e) {
+            if (e.errorCode == ImsException.CODE_ERROR_UNSUPPORTED_OPERATION) {
+                return false;
+            }
+            throw e;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
     /**
      * Registers for updates to the RcsFeature connection through the IImsServiceFeatureCallback
      * callback.
diff --git a/src/com/android/services/telephony/rcs/SipTransportController.java b/src/com/android/services/telephony/rcs/SipTransportController.java
new file mode 100644
index 0000000..da5374a
--- /dev/null
+++ b/src/com/android/services/telephony/rcs/SipTransportController.java
@@ -0,0 +1,195 @@
+/*
+ * 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 android.content.Context;
+import android.telephony.ims.ImsException;
+import android.telephony.ims.ImsService;
+import android.util.Log;
+
+import com.android.ims.RcsFeatureManager;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+
+/**
+ * Manages the creation and destruction of SipDelegates in response to an IMS application requesting
+ * a SipDelegateConnection registered to one or more IMS feature tags.
+ * <p>
+ * This allows an IMS application to forward traffic related to those feature tags over the existing
+ * IMS registration managed by the {@link ImsService} associated with this cellular subscription
+ * instead of requiring that the IMS application manage its own IMS registration over-the-top. This
+ * is required for some cellular carriers, which mandate that all IMS SIP traffic must be sent
+ * through a single IMS registration managed by the system IMS service.
+ */
+public class SipTransportController implements RcsFeatureController.Feature {
+    private static final String LOG_TAG = "SipTransportC";
+
+    private final Context mContext;
+    private final int mSlotId;
+    private final ScheduledExecutorService mExecutorService;
+
+    private int mSubId;
+    private RcsFeatureManager mRcsManager;
+
+    /**
+     * Create an instance of SipTransportController.
+     * @param context The Context associated with this controller.
+     * @param slotId The slot index associated with this controller.
+     * @param subId The subscription ID associated with this controller when it was first created.
+     */
+    public SipTransportController(Context context, int slotId, int subId) {
+        mContext = context;
+        mSlotId = slotId;
+        mSubId = subId;
+
+        mExecutorService = Executors.newSingleThreadScheduledExecutor();
+    }
+
+    /**
+     * Constructor to inject dependencies for testing.
+     */
+    @VisibleForTesting
+    public SipTransportController(Context context, int slotId, int subId,
+            ScheduledExecutorService executor) {
+        mContext = context;
+        mSlotId = slotId;
+        mSubId = subId;
+
+        mExecutorService = executor;
+        logi("created");
+    }
+
+    @Override
+    public void onRcsConnected(RcsFeatureManager manager) {
+        mExecutorService.submit(() -> onRcsManagerChanged(manager));
+    }
+
+    @Override
+    public void onRcsDisconnected() {
+        mExecutorService.submit(() -> onRcsManagerChanged(null));
+    }
+
+    @Override
+    public void onAssociatedSubscriptionUpdated(int subId) {
+        mExecutorService.submit(()-> onSubIdChanged(subId));
+    }
+
+    @Override
+    public void onDestroy() {
+        // Can be null in testing.
+        mExecutorService.shutdownNow();
+    }
+
+    /**
+     * @return Whether or not SipTransports are supported on the connected ImsService. This can
+     * change based on the capabilities of the ImsService.
+     * @throws ImsException if the ImsService connected to this controller is currently down.
+     */
+    public boolean isSupported(int subId) throws ImsException {
+        Boolean result = waitForMethodToComplete(() -> isSupportedInternal(subId));
+        if (result == null) {
+            logw("isSupported, unexpected null result, returning false");
+            return false;
+        }
+        return result;
+    }
+
+    /**
+     * Returns whether or not the ImsService implementation associated with the supplied subId
+     * supports the SipTransport APIs.
+     * <p>
+     * This should only be called on the ExecutorService.
+     * @return true if SipTransport is supported on this subscription, false otherwise.
+     * @throws ImsException thrown if there was an error determining the state of the ImsService.
+     */
+    private boolean isSupportedInternal(int subId) throws ImsException {
+        checkStateOfController(subId);
+        return (mRcsManager.getSipTransport() != null);
+    }
+
+    /**
+     * Run a Callable on the ExecutorService Thread and wait for the result.
+     * If an ImsException is thrown, catch it and rethrow it to caller.
+     */
+    private <T> T waitForMethodToComplete(Callable<T> callable) throws ImsException {
+        Future<T> r = mExecutorService.submit(callable);
+        T result;
+        try {
+            result = r.get();
+        } catch (InterruptedException e) {
+            result = null;
+        } catch (ExecutionException e) {
+            Throwable cause = e.getCause();
+            if (cause instanceof ImsException) {
+                // Rethrow the exception
+                throw (ImsException) cause;
+            }
+            logw("Unexpected Exception, returning null: " + cause);
+            result = null;
+        }
+        return result;
+    }
+
+    /**
+     * Throw an ImsException for common scenarios where the state of the controller is not ready
+     * for communication.
+     * <p>
+     * This should only be called while running on the on the ExecutorService.
+     */
+    private void checkStateOfController(int subId) throws ImsException {
+        if (mSubId != subId) {
+            // sub ID has changed while this was in the queue.
+            throw new ImsException("subId is no longer valid for this request.",
+                    ImsException.CODE_ERROR_INVALID_SUBSCRIPTION);
+        }
+        if (mRcsManager == null) {
+            throw new ImsException("Connection to ImsService is not available",
+                    ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
+        }
+    }
+
+    private void onRcsManagerChanged(RcsFeatureManager m) {
+        logi("manager changed, " + mRcsManager + "->" + m);
+        mRcsManager = m;
+    }
+
+    /**
+     * Called when either the sub ID associated with the slot has changed or the carrier
+     * configuration associated with the same subId has changed.
+     */
+    private void onSubIdChanged(int newSubId) {
+        logi("subId changed, " + mSubId + "->" + newSubId);
+        mSubId = newSubId;
+    }
+
+    private void logi(String message) {
+        Log.i(LOG_TAG, getPrefix() + ": " + message);
+    }
+
+    private void logw(String message) {
+        Log.w(LOG_TAG, getPrefix() + ": " + message);
+    }
+
+    private String getPrefix() {
+        return "[" + mSlotId + "," + mSubId + "]";
+    }
+}
diff --git a/src/com/android/services/telephony/rcs/TelephonyRcsService.java b/src/com/android/services/telephony/rcs/TelephonyRcsService.java
index 69d8f82..79170af 100644
--- a/src/com/android/services/telephony/rcs/TelephonyRcsService.java
+++ b/src/com/android/services/telephony/rcs/TelephonyRcsService.java
@@ -62,6 +62,12 @@
          */
         UserCapabilityExchangeImpl createUserCapabilityExchange(Context context, int slotId,
                 int subId);
+
+        /**
+         * @return an instance of {@link SipTransportController} for the slot and subscription
+         * specified.
+         */
+        SipTransportController createSipTransportController(Context context, int slotId, int subId);
     }
 
     private FeatureFactory mFeatureFactory = new FeatureFactory() {
@@ -75,6 +81,12 @@
                 int subId) {
             return new UserCapabilityExchangeImpl(context, slotId, subId);
         }
+
+        @Override
+        public SipTransportController createSipTransportController(Context context, int slotId,
+                int subId) {
+            return new SipTransportController(context, slotId, subId);
+        }
     };
 
     // Notifies this service that there has been a change in available slots.
@@ -234,6 +246,17 @@
                 c.removeFeature(UserCapabilityExchangeImpl.class);
             }
         }
+
+        if (doesSubscriptionSupportSingleRegistration(subId)) {
+            if (c.getFeature(SipTransportController.class) == null) {
+                c.addFeature(mFeatureFactory.createSipTransportController(mContext, slotId, subId),
+                        SipTransportController.class);
+            }
+        } else {
+            if (c.getFeature(SipTransportController.class) != null) {
+                c.removeFeature(SipTransportController.class);
+            }
+        }
         // Only start the connection procedure if we have active features.
         if (c.hasActiveFeatures()) c.connect();
     }
@@ -250,6 +273,14 @@
         return supportsUce;
     }
 
+    private boolean doesSubscriptionSupportSingleRegistration(int subId) {
+        if (!SubscriptionManager.isValidSubscriptionId(subId)) return false;
+        CarrierConfigManager carrierConfigManager =
+                mContext.getSystemService(CarrierConfigManager.class);
+        if (carrierConfigManager == null) return false;
+        return carrierConfigManager.getConfigForSubId(subId).getBoolean(
+                CarrierConfigManager.Ims.KEY_IMS_SINGLE_REGISTRATION_REQUIRED_BOOL);
+    }
 
     private int getSubscriptionFromSlot(int slotId) {
         SubscriptionManager manager = mContext.getSystemService(SubscriptionManager.class);
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);
     }