Merge "Declare EAB provider in Telephony service"
diff --git a/Android.bp b/Android.bp
index 1887a8a..e7ca068 100644
--- a/Android.bp
+++ b/Android.bp
@@ -35,6 +35,7 @@
         "com.android.phone.common-lib",
         "guava",
         "PlatformProperties",
+        "modules-utils-os",
     ],
 
     srcs: [
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 92b15b0..e38ea00 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -24,7 +24,6 @@
 
     <original-package android:name="com.android.phone" />
 
-    <protected-broadcast android:name="android.telecom.action.TTY_PREFERRED_MODE_CHANGED" />
     <protected-broadcast android:name="android.telecom.action.CURRENT_TTY_MODE_CHANGED" />
     <protected-broadcast android:name="android.intent.action.SERVICE_STATE" />
     <protected-broadcast android:name="android.intent.action.RADIO_TECHNOLOGY" />
@@ -83,6 +82,7 @@
     <protected-broadcast android:name= "com.android.cellbroadcastreceiver.GET_LATEST_CB_AREA_INFO" />
     <protected-broadcast android:name= "com.android.internal.telephony.ACTION_CARRIER_CERTIFICATE_DOWNLOAD" />
     <protected-broadcast android:name= "com.android.internal.telephony.OPEN_DEFAULT_SMS_APP" />
+    <protected-broadcast android:name= "com.android.internal.telephony.ACTION_TEST_OVERRIDE_CARRIER_ID" />
     <protected-broadcast android:name= "android.telephony.action.SIM_CARD_STATE_CHANGED" />
     <protected-broadcast android:name= "android.telephony.action.SIM_APPLICATION_STATE_CHANGED" />
     <protected-broadcast android:name= "android.telephony.action.SIM_SLOT_STATUS_CHANGED" />
@@ -219,6 +219,8 @@
     <uses-permission android:name="android.permission.START_ACTIVITIES_FROM_BACKGROUND" />
     <uses-permission android:name="android.permission.NETWORK_STATS_PROVIDER" />
     <uses-permission android:name="android.permission.HANDLE_CAR_MODE_CHANGES"/>
+    <uses-permission android:name="android.permission.MANAGE_SUBSCRIPTION_PLANS"/>
+    <uses-permission android:name="android.permission.OBSERVE_ROLE_HOLDERS"/>
 
     <application android:name="PhoneApp"
             android:persistent="true"
diff --git a/src/com/android/phone/ImsRcsController.java b/src/com/android/phone/ImsRcsController.java
index fd61936..52069b8 100644
--- a/src/com/android/phone/ImsRcsController.java
+++ b/src/com/android/phone/ImsRcsController.java
@@ -16,19 +16,26 @@
 
 package com.android.phone;
 
+import android.content.pm.PackageManager;
 import android.net.Uri;
 import android.os.Binder;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
+import android.os.UserHandle;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyFrameworkInitializer;
+import android.telephony.ims.DelegateRequest;
 import android.telephony.ims.ImsException;
+import android.telephony.ims.RcsUceAdapter.PublishState;
 import android.telephony.ims.RegistrationManager;
 import android.telephony.ims.aidl.IImsCapabilityCallback;
 import android.telephony.ims.aidl.IImsRcsController;
 import android.telephony.ims.aidl.IImsRegistrationCallback;
 import android.telephony.ims.aidl.IRcsUceControllerCallback;
 import android.telephony.ims.aidl.IRcsUcePublishStateCallback;
+import android.telephony.ims.aidl.ISipDelegate;
+import android.telephony.ims.aidl.ISipDelegateConnectionStateCallback;
+import android.telephony.ims.aidl.ISipDelegateMessageCallback;
 import android.telephony.ims.feature.ImsFeature;
 import android.telephony.ims.feature.RcsFeature;
 import android.telephony.ims.stub.ImsRegistrationImplBase;
@@ -44,7 +51,7 @@
 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;
+import com.android.services.telephony.rcs.UceControllerManager;
 
 import java.util.List;
 
@@ -203,40 +210,6 @@
         }
     }
 
-    @Override
-    public void registerUcePublishStateCallback(int subId, IRcsUcePublishStateCallback c) {
-        enforceReadPrivilegedPermission("registerUcePublishStateCallback");
-        final long token = Binder.clearCallingIdentity();
-        try {
-            UserCapabilityExchangeImpl uce = getRcsFeatureController(subId).getFeature(
-                    UserCapabilityExchangeImpl.class);
-            if (uce == null) {
-                throw new ServiceSpecificException(ImsException.CODE_ERROR_UNSUPPORTED_OPERATION,
-                    "This subscription does not support UCE.");
-            }
-            uce.registerPublishStateCallback(c);
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    @Override
-    public void unregisterUcePublishStateCallback(int subId, IRcsUcePublishStateCallback c) {
-        enforceReadPrivilegedPermission("unregisterUcePublishStateCallback");
-        final long token = Binder.clearCallingIdentity();
-        try {
-            UserCapabilityExchangeImpl uce = getRcsFeatureController(subId).getFeature(
-                    UserCapabilityExchangeImpl.class);
-            if (uce == null) {
-                throw new ServiceSpecificException(ImsException.CODE_ERROR_UNSUPPORTED_OPERATION,
-                    "This subscription does not support UCE.");
-            }
-            uce.unregisterUcePublishStateCallback(c);
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
     /**
      * Query for the capability of an IMS RCS service
      *
@@ -296,30 +269,94 @@
         }
         final long token = Binder.clearCallingIdentity();
         try {
-            UserCapabilityExchangeImpl uce = getRcsFeatureController(subId).getFeature(
-                    UserCapabilityExchangeImpl.class);
-            if (uce == null) {
+            UceControllerManager uceCtrlManager = getRcsFeatureController(subId).getFeature(
+                    UceControllerManager.class);
+            if (uceCtrlManager == null) {
                 throw new ServiceSpecificException(ImsException.CODE_ERROR_UNSUPPORTED_OPERATION,
                         "This subscription does not support UCE.");
             }
-            uce.requestCapabilities(contactNumbers, c);
+            uceCtrlManager.requestCapabilities(contactNumbers, c);
+        } catch (ImsException e) {
+            throw new ServiceSpecificException(e.getCode(), e.getMessage());
         } finally {
             Binder.restoreCallingIdentity(token);
         }
     }
 
     @Override
-    public int getUcePublishState(int subId) {
-        enforceReadPrivilegedPermission("getUcePublishState");
+    public void requestNetworkAvailability(int subId, String callingPackage,
+            String callingFeatureId, Uri contactNumber, IRcsUceControllerCallback c) {
+        enforceReadPrivilegedPermission("requestNetworkAvailability");
+        if (!isUceSettingEnabled(subId, callingPackage, callingFeatureId)) {
+            throw new ServiceSpecificException(ImsException.CODE_ERROR_UNSUPPORTED_OPERATION,
+                    "The user has not enabled UCE for this subscription.");
+        }
         final long token = Binder.clearCallingIdentity();
         try {
-            UserCapabilityExchangeImpl uce = getRcsFeatureController(subId).getFeature(
-                    UserCapabilityExchangeImpl.class);
-            if (uce == null) {
+            UceControllerManager uceCtrlManager = getRcsFeatureController(subId).getFeature(
+                    UceControllerManager.class);
+            if (uceCtrlManager == null) {
                 throw new ServiceSpecificException(ImsException.CODE_ERROR_UNSUPPORTED_OPERATION,
                         "This subscription does not support UCE.");
             }
-            return uce.getUcePublishState();
+            uceCtrlManager.requestNetworkAvailability(contactNumber, c);
+        } catch (ImsException e) {
+            throw new ServiceSpecificException(e.getCode(), e.getMessage());
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    @Override
+    public @PublishState int getUcePublishState(int subId) {
+        enforceReadPrivilegedPermission("getUcePublishState");
+        final long token = Binder.clearCallingIdentity();
+        try {
+            UceControllerManager uceCtrlManager = getRcsFeatureController(subId).getFeature(
+                    UceControllerManager.class);
+            if (uceCtrlManager == null) {
+                throw new ServiceSpecificException(ImsException.CODE_ERROR_UNSUPPORTED_OPERATION,
+                        "This subscription does not support UCE.");
+            }
+            return uceCtrlManager.getUcePublishState();
+        } catch (ImsException e) {
+            throw new ServiceSpecificException(e.getCode(), e.getMessage());
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    @Override
+    public void registerUcePublishStateCallback(int subId, IRcsUcePublishStateCallback c) {
+        enforceReadPrivilegedPermission("registerUcePublishStateCallback");
+        final long token = Binder.clearCallingIdentity();
+        try {
+            UceControllerManager uceCtrlManager = getRcsFeatureController(subId).getFeature(
+                    UceControllerManager.class);
+            if (uceCtrlManager == null) {
+                throw new ServiceSpecificException(ImsException.CODE_ERROR_UNSUPPORTED_OPERATION,
+                        "This subscription does not support UCE.");
+            }
+            uceCtrlManager.registerPublishStateCallback(c);
+        } catch (ImsException e) {
+            throw new ServiceSpecificException(e.getCode(), e.getMessage());
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    @Override
+    public void unregisterUcePublishStateCallback(int subId, IRcsUcePublishStateCallback c) {
+        enforceReadPrivilegedPermission("unregisterUcePublishStateCallback");
+        final long token = Binder.clearCallingIdentity();
+        try {
+            UceControllerManager uceCtrlManager = getRcsFeatureController(subId).getFeature(
+                    UceControllerManager.class);
+            if (uceCtrlManager == null) {
+                throw new ServiceSpecificException(ImsException.CODE_ERROR_UNSUPPORTED_OPERATION,
+                        "This subscription does not support UCE.");
+            }
+            uceCtrlManager.unregisterPublishStateCallback(c);
         } finally {
             Binder.restoreCallingIdentity(token);
         }
@@ -377,6 +414,58 @@
         }
     }
 
+    @Override
+    public void createSipDelegate(int subId, DelegateRequest request, String packageName,
+            ISipDelegateConnectionStateCallback delegateState,
+            ISipDelegateMessageCallback delegateMessage) {
+        enforceModifyPermission();
+        if (!UserHandle.getUserHandleForUid(Binder.getCallingUid()).isSystem()) {
+            throw new ServiceSpecificException(ImsException.CODE_ERROR_UNSUPPORTED_OPERATION,
+                    "SipDelegate creation is only available to primary user.");
+        }
+        try {
+            int remoteUid = mApp.getPackageManager().getPackageUid(packageName, 0 /*flags*/);
+            if (Binder.getCallingUid() != remoteUid) {
+                throw new SecurityException("passed in packageName does not match the caller");
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            throw new SecurityException("Passed in PackageName can not be found on device");
+        }
+
+        final long identity = Binder.clearCallingIdentity();
+        SipTransportController transport = getRcsFeatureController(subId).getFeature(
+                SipTransportController.class);
+        if (transport == null) {
+            throw new ServiceSpecificException(ImsException.CODE_ERROR_UNSUPPORTED_OPERATION,
+                    "This subscription does not support the creation of SIP delegates");
+        }
+        try {
+            transport.createSipDelegate(subId, request, packageName, delegateState,
+                    delegateMessage);
+        } catch (ImsException e) {
+            throw new ServiceSpecificException(e.getCode(), e.getMessage());
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    @Override
+    public void destroySipDelegate(int subId, ISipDelegate connection, int reason) {
+        enforceModifyPermission();
+
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            SipTransportController transport = getRcsFeatureController(subId).getFeature(
+                    SipTransportController.class);
+            if (transport == null) {
+                return;
+            }
+            transport.destroySipDelegate(subId, connection, reason);
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
     /**
      * Registers for updates to the RcsFeature connection through the IImsServiceFeatureCallback
      * callback.
diff --git a/src/com/android/phone/ImsUtil.java b/src/com/android/phone/ImsUtil.java
index 38936ec..0825cfb 100644
--- a/src/com/android/phone/ImsUtil.java
+++ b/src/com/android/phone/ImsUtil.java
@@ -25,6 +25,8 @@
 
 import com.android.ims.ImsConfig;
 import com.android.ims.ImsManager;
+import com.android.internal.telephony.PhoneFactory;
+import com.android.internal.telephony.imsphone.ImsPhone;
 
 public class ImsUtil {
     private static final String LOG_TAG = ImsUtil.class.getSimpleName();
@@ -128,6 +130,13 @@
             return false;
         }
 
+        // Do not promote WFC if in roaming and WFC roaming not allowed.
+        // WFC roaming setting is not modifiable, so its value is decided by the default value
+        // chosen by the carrier, hence it really means if the carrier supports WFC roaming.
+        if (getLastKnownRoamingState(phoneId) && !imsManager.isWfcRoamingEnabledByUser()) {
+            return false;
+        }
+
         ConnectivityManager cm =
                 (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
         if (cm != null) {
@@ -152,4 +161,13 @@
         }
         return subId;
     }
+
+    private static boolean getLastKnownRoamingState(int phoneId) {
+        try {
+            ImsPhone imsPhone = (ImsPhone) (PhoneFactory.getPhone(phoneId).getImsPhone());
+            return imsPhone.getRoamingState();
+        } catch (NullPointerException | ClassCastException e) {
+            return false;
+        }
+    }
 }
diff --git a/src/com/android/phone/NotificationMgr.java b/src/com/android/phone/NotificationMgr.java
index c2dece5..fe4a0ba 100644
--- a/src/com/android/phone/NotificationMgr.java
+++ b/src/com/android/phone/NotificationMgr.java
@@ -536,7 +536,11 @@
                 int slotId = SubscriptionManager.getSlotIndex(subId);
                 resId = (slotId == 0) ? R.drawable.stat_sys_phone_call_forward_sub1
                         : R.drawable.stat_sys_phone_call_forward_sub2;
-                notificationTitle = subInfo.getDisplayName().toString();
+                if (subInfo.getDisplayName() != null) {
+                    notificationTitle = subInfo.getDisplayName().toString();
+                } else {
+                    notificationTitle = mContext.getString(R.string.labelCF);
+                }
             } else {
                 notificationTitle = mContext.getString(R.string.labelCF);
             }
diff --git a/src/com/android/phone/PhoneInterfaceManager.java b/src/com/android/phone/PhoneInterfaceManager.java
index a28130d..36ac63e 100755
--- a/src/com/android/phone/PhoneInterfaceManager.java
+++ b/src/com/android/phone/PhoneInterfaceManager.java
@@ -65,6 +65,7 @@
 import android.telecom.TelecomManager;
 import android.telephony.Annotation.ApnType;
 import android.telephony.CallForwardingInfo;
+import android.telephony.CarrierBandwidth;
 import android.telephony.CarrierConfigManager;
 import android.telephony.CarrierRestrictionRules;
 import android.telephony.CellIdentity;
@@ -130,6 +131,7 @@
 import com.android.internal.telephony.DefaultPhoneNotifier;
 import com.android.internal.telephony.HalVersion;
 import com.android.internal.telephony.IBooleanConsumer;
+import com.android.internal.telephony.ICallForwardingInfoCallback;
 import com.android.internal.telephony.IIntegerConsumer;
 import com.android.internal.telephony.INumberVerificationCallback;
 import com.android.internal.telephony.ITelephony;
@@ -170,6 +172,7 @@
 import com.android.internal.telephony.uicc.UiccSlot;
 import com.android.internal.telephony.util.LocaleUtils;
 import com.android.internal.telephony.util.VoicemailNotificationSettingsUtil;
+import com.android.internal.util.FunctionalUtils;
 import com.android.internal.util.HexDump;
 import com.android.phone.settings.PickSmsSubscriptionActivity;
 import com.android.phone.vvm.PhoneAccountHandleConverter;
@@ -287,6 +290,12 @@
     private static final int EVENT_GET_CALL_WAITING_DONE = 88;
     private static final int CMD_SET_CALL_WAITING = 89;
     private static final int EVENT_SET_CALL_WAITING_DONE = 90;
+    private static final int CMD_ENABLE_NR_DUAL_CONNECTIVITY = 91;
+    private static final int EVENT_ENABLE_NR_DUAL_CONNECTIVITY_DONE = 92;
+    private static final int CMD_IS_NR_DUAL_CONNECTIVITY_ENABLED = 93;
+    private static final int EVENT_IS_NR_DUAL_CONNECTIVITY_ENABLED_DONE = 94;
+    private static final int CMD_GET_CDMA_SUBSCRIPTION_MODE = 95;
+    private static final int EVENT_GET_CDMA_SUBSCRIPTION_MODE_DONE = 96;
 
     // Parameters of select command.
     private static final int SELECT_COMMAND = 0xA4;
@@ -749,6 +758,90 @@
                     handleNullReturnEvent(msg, "resetModemConfig");
                     break;
 
+                case CMD_IS_NR_DUAL_CONNECTIVITY_ENABLED: {
+                    request = (MainThreadRequest) msg.obj;
+                    onCompleted = obtainMessage(EVENT_IS_NR_DUAL_CONNECTIVITY_ENABLED_DONE,
+                            request);
+                    Phone phone = getPhoneFromRequest(request);
+                    if (phone != null) {
+                        phone.isNrDualConnectivityEnabled(onCompleted, request.workSource);
+                    } else {
+                        loge("isNRDualConnectivityEnabled: No phone object");
+                        request.result = false;
+                        notifyRequester(request);
+                    }
+                    break;
+                }
+
+                case EVENT_IS_NR_DUAL_CONNECTIVITY_ENABLED_DONE:
+                    ar = (AsyncResult) msg.obj;
+                    request = (MainThreadRequest) ar.userObj;
+                    if (ar.exception == null && ar.result != null) {
+                        request.result = ar.result;
+                    } else {
+                        // request.result must be set to something non-null
+                        // for the calling thread to unblock
+                        if (request.result != null) {
+                            request.result = ar.result;
+                        } else {
+                            request.result = false;
+                        }
+                        if (ar.result == null) {
+                            loge("isNRDualConnectivityEnabled: Empty response");
+                        } else if (ar.exception instanceof CommandException) {
+                            loge("isNRDualConnectivityEnabled: CommandException: "
+                                    + ar.exception);
+                        } else {
+                            loge("isNRDualConnectivityEnabled: Unknown exception");
+                        }
+                    }
+                    notifyRequester(request);
+                    break;
+
+                case CMD_ENABLE_NR_DUAL_CONNECTIVITY: {
+                    request = (MainThreadRequest) msg.obj;
+                    onCompleted = obtainMessage(EVENT_ENABLE_NR_DUAL_CONNECTIVITY_DONE, request);
+                    Phone phone = getPhoneFromRequest(request);
+                    if (phone != null) {
+                        phone.setNrDualConnectivityState((int) request.argument, onCompleted,
+                                request.workSource);
+                    } else {
+                        loge("enableNrDualConnectivity: No phone object");
+                        request.result =
+                                TelephonyManager.ENABLE_NR_DUAL_CONNECTIVITY_RADIO_NOT_AVAILABLE;
+                        notifyRequester(request);
+                    }
+                    break;
+                }
+
+                case EVENT_ENABLE_NR_DUAL_CONNECTIVITY_DONE: {
+                    ar = (AsyncResult) msg.obj;
+                    request = (MainThreadRequest) ar.userObj;
+                    if (ar.exception == null) {
+                        request.result =
+                                TelephonyManager.ENABLE_NR_DUAL_CONNECTIVITY_SUCCESS;
+                    } else {
+                        request.result =
+                                TelephonyManager
+                                        .ENABLE_NR_DUAL_CONNECTIVITY_RADIO_ERROR;
+                        if (ar.exception instanceof CommandException) {
+                            CommandException.Error error =
+                                    ((CommandException) (ar.exception)).getCommandError();
+                            if (error == CommandException.Error.RADIO_NOT_AVAILABLE) {
+                                request.result =
+                                        TelephonyManager
+                                                .ENABLE_NR_DUAL_CONNECTIVITY_RADIO_NOT_AVAILABLE;
+                            }
+                            loge("enableNrDualConnectivity" + ": CommandException: "
+                                    + ar.exception);
+                        } else {
+                            loge("enableNrDualConnectivity" + ": Unknown exception");
+                        }
+                    }
+                    notifyRequester(request);
+                    break;
+                }
+
                 case CMD_GET_PREFERRED_NETWORK_TYPE:
                     request = (MainThreadRequest) msg.obj;
                     onCompleted = obtainMessage(EVENT_GET_PREFERRED_NETWORK_TYPE_DONE, request);
@@ -829,38 +922,43 @@
                     getPhoneFromRequest(request).getAvailableNetworks(onCompleted);
                     break;
 
-                case CMD_GET_CALL_FORWARDING:
+                case CMD_GET_CALL_FORWARDING: {
                     request = (MainThreadRequest) msg.obj;
                     onCompleted = obtainMessage(EVENT_GET_CALL_FORWARDING_DONE, request);
-                    int callForwardingReason = (Integer) request.argument;
-                    getPhoneFromRequest(request).getCallForwardingOption(
-                            callForwardingReason, onCompleted);
+                    Pair<Integer, TelephonyManager.CallForwardingInfoCallback> args =
+                            (Pair<Integer, TelephonyManager.CallForwardingInfoCallback>)
+                                    request.argument;
+                    int callForwardingReason = args.first;
+                    request.phone.getCallForwardingOption(callForwardingReason, onCompleted);
                     break;
-
-                case EVENT_GET_CALL_FORWARDING_DONE:
+                }
+                case EVENT_GET_CALL_FORWARDING_DONE: {
                     ar = (AsyncResult) msg.obj;
                     request = (MainThreadRequest) ar.userObj;
-                    CallForwardingInfo callForwardingInfo = null;
+                    TelephonyManager.CallForwardingInfoCallback callback =
+                            ((Pair<Integer, TelephonyManager.CallForwardingInfoCallback>)
+                                    request.argument).second;
                     if (ar.exception == null && ar.result != null) {
+                        CallForwardingInfo callForwardingInfo = null;
                         CallForwardInfo[] callForwardInfos = (CallForwardInfo[]) ar.result;
                         for (CallForwardInfo callForwardInfo : callForwardInfos) {
                             // Service Class is a bit mask per 3gpp 27.007. Search for
                             // any service for voice call.
                             if ((callForwardInfo.serviceClass
                                     & CommandsInterface.SERVICE_CLASS_VOICE) > 0) {
-                                callForwardingInfo = new CallForwardingInfo(
-                                        callForwardInfo.serviceClass, callForwardInfo.reason,
-                                                callForwardInfo.number,
-                                                        callForwardInfo.timeSeconds);
+                                callForwardingInfo = new CallForwardingInfo(true,
+                                        callForwardInfo.reason,
+                                        callForwardInfo.number,
+                                        callForwardInfo.timeSeconds);
                                 break;
                             }
                         }
                         // Didn't find a call forward info for voice call.
                         if (callForwardingInfo == null) {
-                            callForwardingInfo = new CallForwardingInfo(
-                                    CallForwardingInfo.STATUS_UNKNOWN_ERROR,
-                                            0 /* reason */, null /* number */, 0 /* timeout */);
+                            callForwardingInfo = new CallForwardingInfo(false /* enabled */,
+                                    0 /* reason */, null /* number */, 0 /* timeout */);
                         }
+                        callback.onCallForwardingInfoAvailable(callForwardingInfo);
                     } else {
                         if (ar.result == null) {
                             loge("EVENT_GET_CALL_FORWARDING_DONE: Empty response");
@@ -868,56 +966,80 @@
                         if (ar.exception != null) {
                             loge("EVENT_GET_CALL_FORWARDING_DONE: Exception: " + ar.exception);
                         }
-                        int errorCode = CallForwardingInfo.STATUS_UNKNOWN_ERROR;
+                        int errorCode = TelephonyManager
+                                .CallForwardingInfoCallback.RESULT_ERROR_UNKNOWN;
                         if (ar.exception instanceof CommandException) {
                             CommandException.Error error =
                                     ((CommandException) (ar.exception)).getCommandError();
                             if (error == CommandException.Error.FDN_CHECK_FAILURE) {
-                                errorCode = CallForwardingInfo.STATUS_FDN_CHECK_FAILURE;
+                                errorCode = TelephonyManager
+                                        .CallForwardingInfoCallback.RESULT_ERROR_FDN_CHECK_FAILURE;
                             } else if (error == CommandException.Error.REQUEST_NOT_SUPPORTED) {
-                                errorCode = CallForwardingInfo.STATUS_NOT_SUPPORTED;
+                                errorCode = TelephonyManager
+                                        .CallForwardingInfoCallback.RESULT_ERROR_NOT_SUPPORTED;
                             }
                         }
-                        callForwardingInfo = new CallForwardingInfo(
-                                errorCode, 0 /* reason */, null /* number */, 0 /* timeout */);
+                        callback.onError(errorCode);
                     }
-                    request.result = callForwardingInfo;
-                    notifyRequester(request);
                     break;
+                }
 
-                case CMD_SET_CALL_FORWARDING:
+                case CMD_SET_CALL_FORWARDING: {
                     request = (MainThreadRequest) msg.obj;
                     onCompleted = obtainMessage(EVENT_SET_CALL_FORWARDING_DONE, request);
+                    request = (MainThreadRequest) msg.obj;
                     CallForwardingInfo callForwardingInfoToSet =
-                            (CallForwardingInfo) request.argument;
-                    getPhoneFromRequest(request).setCallForwardingOption(
-                            callForwardingInfoToSet.getStatus(),
+                            ((Pair<CallForwardingInfo, Consumer<Integer>>)
+                                    request.argument).first;
+                    request.phone.setCallForwardingOption(
+                            callForwardingInfoToSet.isEnabled()
+                                    ? CommandsInterface.CF_ACTION_ENABLE
+                                    : CommandsInterface.CF_ACTION_DISABLE,
                             callForwardingInfoToSet.getReason(),
                             callForwardingInfoToSet.getNumber(),
                             callForwardingInfoToSet.getTimeoutSeconds(), onCompleted);
                     break;
+                }
 
-                case EVENT_SET_CALL_FORWARDING_DONE:
+                case EVENT_SET_CALL_FORWARDING_DONE: {
                     ar = (AsyncResult) msg.obj;
                     request = (MainThreadRequest) ar.userObj;
-                    if (ar.exception == null) {
-                        request.result = true;
-                    } else {
-                        request.result = false;
+                    Consumer<Integer> callback =
+                            ((Pair<CallForwardingInfo, Consumer<Integer>>)
+                                    request.argument).second;
+                    if (ar.exception != null) {
                         loge("setCallForwarding exception: " + ar.exception);
+                        int errorCode = TelephonyManager.CallForwardingInfoCallback
+                                .RESULT_ERROR_UNKNOWN;
+                        if (ar.exception instanceof CommandException) {
+                            CommandException.Error error =
+                                    ((CommandException) (ar.exception)).getCommandError();
+                            if (error == CommandException.Error.FDN_CHECK_FAILURE) {
+                                errorCode = TelephonyManager.CallForwardingInfoCallback
+                                        .RESULT_ERROR_FDN_CHECK_FAILURE;
+                            } else if (error == CommandException.Error.REQUEST_NOT_SUPPORTED) {
+                                errorCode = TelephonyManager.CallForwardingInfoCallback
+                                        .RESULT_ERROR_NOT_SUPPORTED;
+                            }
+                        }
+                        callback.accept(errorCode);
+                    } else {
+                        callback.accept(TelephonyManager.CallForwardingInfoCallback.RESULT_SUCCESS);
                     }
-                    notifyRequester(request);
                     break;
+                }
 
-                case CMD_GET_CALL_WAITING:
+                case CMD_GET_CALL_WAITING: {
                     request = (MainThreadRequest) msg.obj;
                     onCompleted = obtainMessage(EVENT_GET_CALL_WAITING_DONE, request);
                     getPhoneFromRequest(request).getCallWaiting(onCompleted);
                     break;
+                }
 
-                case EVENT_GET_CALL_WAITING_DONE:
+                case EVENT_GET_CALL_WAITING_DONE: {
                     ar = (AsyncResult) msg.obj;
                     request = (MainThreadRequest) ar.userObj;
+                    Consumer<Integer> callback = (Consumer<Integer>) request.argument;
                     int callForwardingStatus = TelephonyManager.CALL_WAITING_STATUS_UNKNOWN_ERROR;
                     if (ar.exception == null && ar.result != null) {
                         int[] callForwardResults = (int[]) ar.result;
@@ -925,12 +1047,12 @@
                         // Search for any service for voice call.
                         if (callForwardResults.length > 1
                                 && ((callForwardResults[1]
-                                        & CommandsInterface.SERVICE_CLASS_VOICE) > 0)) {
+                                & CommandsInterface.SERVICE_CLASS_VOICE) > 0)) {
                             callForwardingStatus = callForwardResults[0] == 0
-                                    ? TelephonyManager.CALL_WAITING_STATUS_INACTIVE
-                                            : TelephonyManager.CALL_WAITING_STATUS_ACTIVE;
+                                    ? TelephonyManager.CALL_WAITING_STATUS_DISABLED
+                                    : TelephonyManager.CALL_WAITING_STATUS_ENABLED;
                         } else {
-                            callForwardingStatus = TelephonyManager.CALL_WAITING_STATUS_INACTIVE;
+                            callForwardingStatus = TelephonyManager.CALL_WAITING_STATUS_DISABLED;
                         }
                     } else {
                         if (ar.result == null) {
@@ -948,28 +1070,43 @@
                             }
                         }
                     }
-                    request.result = callForwardingStatus;
-                    notifyRequester(request);
+                    callback.accept(callForwardingStatus);
                     break;
+                }
 
-                case CMD_SET_CALL_WAITING:
+                case CMD_SET_CALL_WAITING: {
                     request = (MainThreadRequest) msg.obj;
                     onCompleted = obtainMessage(EVENT_SET_CALL_WAITING_DONE, request);
-                    boolean isEnable = (Boolean) request.argument;
-                    getPhoneFromRequest(request).setCallWaiting(isEnable, onCompleted);
+                    boolean enable = ((Pair<Boolean, Consumer<Integer>>) request.argument).first;
+                    getPhoneFromRequest(request).setCallWaiting(enable, onCompleted);
                     break;
+                }
 
-                case EVENT_SET_CALL_WAITING_DONE:
+                case EVENT_SET_CALL_WAITING_DONE: {
                     ar = (AsyncResult) msg.obj;
                     request = (MainThreadRequest) ar.userObj;
-                    if (ar.exception == null) {
-                        request.result = true;
-                    } else {
-                        request.result = false;
+                    boolean enable = ((Pair<Boolean, Consumer<Integer>>) request.argument).first;
+                    Consumer<Integer> callback =
+                            ((Pair<Boolean, Consumer<Integer>>) request.argument).second;
+                    if (ar.exception != null) {
                         loge("setCallWaiting exception: " + ar.exception);
+                        if (ar.exception instanceof CommandException) {
+                            CommandException.Error error =
+                                    ((CommandException) (ar.exception)).getCommandError();
+                            if (error == CommandException.Error.REQUEST_NOT_SUPPORTED) {
+                                callback.accept(TelephonyManager.CALL_WAITING_STATUS_NOT_SUPPORTED);
+                            } else {
+                                callback.accept(TelephonyManager.CALL_WAITING_STATUS_UNKNOWN_ERROR);
+                            }
+                        } else {
+                            callback.accept(TelephonyManager.CALL_WAITING_STATUS_UNKNOWN_ERROR);
+                        }
+                    } else {
+                        callback.accept(enable ? TelephonyManager.CALL_WAITING_STATUS_ENABLED
+                                : TelephonyManager.CALL_WAITING_STATUS_DISABLED);
                     }
-                    notifyRequester(request);
                     break;
+                }
 
                 case EVENT_PERFORM_NETWORK_SCAN_DONE:
                     ar = (AsyncResult) msg.obj;
@@ -1248,11 +1385,27 @@
                     request.result = ar.exception == null;
                     notifyRequester(request);
                     break;
+                case CMD_GET_CDMA_SUBSCRIPTION_MODE:
+                    request = (MainThreadRequest) msg.obj;
+                    onCompleted = obtainMessage(EVENT_GET_CDMA_SUBSCRIPTION_MODE_DONE, request);
+                    getPhoneFromRequest(request).queryCdmaSubscriptionMode(onCompleted);
+                    break;
+                case EVENT_GET_CDMA_SUBSCRIPTION_MODE_DONE:
+                    ar = (AsyncResult) msg.obj;
+                    request = (MainThreadRequest) ar.userObj;
+                    if (ar.exception != null) {
+                        request.result = TelephonyManager.CDMA_SUBSCRIPTION_RUIM_SIM;
+                    } else {
+                        request.result = ((int[]) ar.result)[0];
+                    }
+                    notifyRequester(request);
+                    break;
                 case CMD_SET_CDMA_SUBSCRIPTION_MODE:
                     request = (MainThreadRequest) msg.obj;
                     onCompleted = obtainMessage(EVENT_SET_CDMA_SUBSCRIPTION_MODE_DONE, request);
                     int subscriptionMode = (int) request.argument;
-                    getPhoneFromRequest(request).setCdmaSubscription(subscriptionMode, onCompleted);
+                    getPhoneFromRequest(request).setCdmaSubscriptionMode(
+                            subscriptionMode, onCompleted);
                     break;
                 case EVENT_SET_CDMA_SUBSCRIPTION_MODE_DONE:
                     ar = (AsyncResult) msg.obj;
@@ -2174,7 +2327,8 @@
             int subId = mSubscriptionController.getDefaultDataSubId();
             final Phone phone = getPhone(subId);
             if (phone != null) {
-                phone.getDataEnabledSettings().setUserDataEnabled(true);
+                phone.getDataEnabledSettings().setDataEnabled(
+                        TelephonyManager.DATA_ENABLED_REASON_USER, true);
                 return true;
             } else {
                 return false;
@@ -2194,7 +2348,8 @@
             int subId = mSubscriptionController.getDefaultDataSubId();
             final Phone phone = getPhone(subId);
             if (phone != null) {
-                phone.getDataEnabledSettings().setUserDataEnabled(false);
+                phone.getDataEnabledSettings().setDataEnabled(
+                        TelephonyManager.DATA_ENABLED_REASON_USER, false);
                 return true;
             } else {
                 return false;
@@ -5116,6 +5271,36 @@
     }
 
     /**
+     * Clears any carrier ImsService overrides for the slot index specified that were previously
+     * set with {@link #setBoundImsServiceOverride(int, boolean, int[], String)}.
+     *
+     * This should only be used for testing.
+     *
+     * @param slotIndex the slot ID that the ImsService should bind for.
+     * @return true if clearing the carrier ImsService override succeeded or false if it did not.
+     */
+    @Override
+    public boolean clearCarrierImsServiceOverride(int slotIndex) {
+        int[] subIds = SubscriptionManager.getSubId(slotIndex);
+        TelephonyPermissions.enforceShellOnly(Binder.getCallingUid(),
+                "clearCarrierImsServiceOverride");
+        TelephonyPermissions.enforceCallingOrSelfModifyPermissionOrCarrierPrivilege(mApp,
+                (subIds != null ? subIds[0] : SubscriptionManager.INVALID_SUBSCRIPTION_ID),
+                "clearCarrierImsServiceOverride");
+
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            if (mImsResolver == null) {
+                // may happen if the device does not support IMS.
+                return false;
+            }
+            return mImsResolver.clearCarrierImsServiceConfiguration(slotIndex);
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    /**
      * Return the package name of the currently bound ImsService.
      *
      * @param slotId The slot that the ImsService is associated with.
@@ -5316,7 +5501,8 @@
      * Get the call forwarding info, given the call forwarding reason.
      */
     @Override
-    public CallForwardingInfo getCallForwarding(int subId, int callForwardingReason) {
+    public void getCallForwarding(int subId, int callForwardingReason,
+            ICallForwardingInfoCallback callback) {
         enforceReadPrivilegedPermission("getCallForwarding");
         long identity = Binder.clearCallingIdentity();
         try {
@@ -5324,8 +5510,39 @@
                 log("getCallForwarding: subId " + subId
                         + " callForwardingReason" + callForwardingReason);
             }
-            return (CallForwardingInfo) sendRequest(
-                    CMD_GET_CALL_FORWARDING, callForwardingReason, subId);
+
+            Phone phone = getPhone(subId);
+            if (phone == null) {
+                try {
+                    callback.onError(
+                            TelephonyManager.CallForwardingInfoCallback.RESULT_ERROR_UNKNOWN);
+                } catch (RemoteException e) {
+                    // ignore
+                }
+                return;
+            }
+
+            Pair<Integer, TelephonyManager.CallForwardingInfoCallback> argument = Pair.create(
+                    callForwardingReason, new TelephonyManager.CallForwardingInfoCallback() {
+                        @Override
+                        public void onCallForwardingInfoAvailable(CallForwardingInfo info) {
+                            try {
+                                callback.onCallForwardingInfoAvailable(info);
+                            } catch (RemoteException e) {
+                                // ignore
+                            }
+                        }
+
+                        @Override
+                        public void onError(int error) {
+                            try {
+                                callback.onError(error);
+                            } catch (RemoteException e) {
+                                // ignore
+                            }
+                        }
+                    });
+            sendRequestAsync(CMD_GET_CALL_FORWARDING, argument, phone, null);
         } finally {
             Binder.restoreCallingIdentity(identity);
         }
@@ -5336,7 +5553,8 @@
      * reason, the number to forward, and the timeout before the forwarding is attempted.
      */
     @Override
-    public boolean setCallForwarding(int subId, CallForwardingInfo callForwardingInfo) {
+    public void setCallForwarding(int subId, CallForwardingInfo callForwardingInfo,
+            IIntegerConsumer callback) {
         enforceModifyPermission();
         long identity = Binder.clearCallingIdentity();
         try {
@@ -5344,38 +5562,79 @@
                 log("setCallForwarding: subId " + subId
                         + " callForwardingInfo" + callForwardingInfo);
             }
-            return (Boolean) sendRequest(CMD_SET_CALL_FORWARDING, callForwardingInfo, subId);
+
+            Phone phone = getPhone(subId);
+            if (phone == null) {
+                try {
+                    callback.accept(
+                            TelephonyManager.CallForwardingInfoCallback.RESULT_ERROR_UNKNOWN);
+                } catch (RemoteException e) {
+                    // ignore
+                }
+                return;
+            }
+
+            Pair<CallForwardingInfo, Consumer<Integer>> arguments = Pair.create(callForwardingInfo,
+                    FunctionalUtils.ignoreRemoteException(callback::accept));
+
+            sendRequestAsync(CMD_SET_CALL_FORWARDING, arguments, phone, null);
         } finally {
             Binder.restoreCallingIdentity(identity);
         }
     }
 
     /**
-     * Get the call forwarding info, given the call forwarding reason.
+     * Get the call waiting status for a subId.
      */
     @Override
-    public int getCallWaitingStatus(int subId) {
+    public void getCallWaitingStatus(int subId, IIntegerConsumer callback) {
         enforceReadPrivilegedPermission("getCallForwarding");
         long identity = Binder.clearCallingIdentity();
         try {
+
+            Phone phone = getPhone(subId);
+            if (phone == null) {
+                try {
+                    callback.accept(TelephonyManager.CALL_WAITING_STATUS_UNKNOWN_ERROR);
+                } catch (RemoteException e) {
+                    // ignore
+                }
+                return;
+            }
+
+            Consumer<Integer> argument = FunctionalUtils.ignoreRemoteException(callback::accept);
+
             if (DBG) log("getCallWaitingStatus: subId " + subId);
-            return (Integer) sendRequest(CMD_GET_CALL_WAITING, null, subId);
+            sendRequestAsync(CMD_GET_CALL_WAITING, argument, phone, null);
         } finally {
             Binder.restoreCallingIdentity(identity);
         }
     }
 
     /**
-     * Sets the voice call forwarding info including status (enable/disable), call forwarding
-     * reason, the number to forward, and the timeout before the forwarding is attempted.
+     * Sets whether call waiting is enabled for a given subId.
      */
     @Override
-    public boolean setCallWaitingStatus(int subId, boolean isEnable) {
+    public void setCallWaitingStatus(int subId, boolean enable, IIntegerConsumer callback) {
         enforceModifyPermission();
         long identity = Binder.clearCallingIdentity();
         try {
-            if (DBG) log("setCallWaitingStatus: subId " + subId + " isEnable: " + isEnable);
-            return (Boolean) sendRequest(CMD_SET_CALL_WAITING, isEnable, subId);
+            if (DBG) log("setCallWaitingStatus: subId " + subId + " enable: " + enable);
+
+            Phone phone = getPhone(subId);
+            if (phone == null) {
+                try {
+                    callback.accept(TelephonyManager.CALL_WAITING_STATUS_UNKNOWN_ERROR);
+                } catch (RemoteException e) {
+                    // ignore
+                }
+                return;
+            }
+
+            Pair<Boolean, Consumer<Integer>> arguments = Pair.create(enable,
+                    FunctionalUtils.ignoreRemoteException(callback::accept));
+
+            sendRequestAsync(CMD_SET_CALL_WAITING, arguments, phone, null);
         } finally {
             Binder.restoreCallingIdentity(identity);
         }
@@ -5406,7 +5665,8 @@
                                 .setMinSdkVersionForFine(Build.VERSION_CODES.Q)
                                 .build());
         if (locationResult != LocationAccessPolicy.LocationPermissionResult.ALLOWED) {
-            SecurityException e = checkNetworkRequestForSanitizedLocationAccess(request, subId);
+            SecurityException e = checkNetworkRequestForSanitizedLocationAccess(
+                    request, subId, callingPackage);
             if (e != null) {
                 if (locationResult == LocationAccessPolicy.LocationPermissionResult.DENIED_HARD) {
                     throw e;
@@ -5429,8 +5689,8 @@
     }
 
     private SecurityException checkNetworkRequestForSanitizedLocationAccess(
-            NetworkScanRequest request, int subId) {
-        boolean hasCarrierPriv = getCarrierPrivilegeStatusForUid(subId, Binder.getCallingUid())
+            NetworkScanRequest request, int subId, String callingPackage) {
+        boolean hasCarrierPriv = checkCarrierPrivilegesForPackage(subId, callingPackage)
                 == TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS;
         boolean hasNetworkScanPermission =
                 mApp.checkCallingOrSelfPermission(android.Manifest.permission.NETWORK_SCAN)
@@ -5610,6 +5870,80 @@
     }
 
     /**
+     * Enable/Disable E-UTRA-NR Dual Connectivity
+     * @param subId subscription id of the sim card
+     * @param nrDualConnectivityState expected NR dual connectivity state
+     * This can be passed following states
+     * <ol>
+     * <li>Enable NR dual connectivity {@link TelephonyManager#NR_DUAL_CONNECTIVITY_ENABLE}
+     * <li>Disable NR dual connectivity {@link TelephonyManager#NR_DUAL_CONNECTIVITY_DISABLE}
+     * <li>Disable NR dual connectivity and force secondary cell to be released
+     * {@link TelephonyManager#NR_DUAL_CONNECTIVITY_DISABLE_IMMEDIATE}
+     * </ol>
+     * @return operation result.
+     */
+    @Override
+    public int setNrDualConnectivityState(int subId,
+            @TelephonyManager.NrDualConnectivityState int nrDualConnectivityState) {
+        TelephonyPermissions.enforceCallingOrSelfModifyPermissionOrCarrierPrivilege(
+                mApp, subId, "enableNRDualConnectivity");
+        WorkSource workSource = getWorkSource(Binder.getCallingUid());
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            int result = (int) sendRequest(CMD_ENABLE_NR_DUAL_CONNECTIVITY,
+                    nrDualConnectivityState, subId,
+                    workSource);
+            if (DBG) log("enableNRDualConnectivity result: " + result);
+            return result;
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    /**
+     * Is E-UTRA-NR Dual Connectivity enabled
+     * @return true if dual connectivity is enabled else false
+     */
+    @Override
+    public boolean isNrDualConnectivityEnabled(int subId) {
+        TelephonyPermissions
+                .enforeceCallingOrSelfReadPrivilegedPhoneStatePermissionOrCarrierPrivilege(
+                        mApp, subId, "isNRDualConnectivityEnabled");
+        WorkSource workSource = getWorkSource(Binder.getCallingUid());
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            boolean isEnabled = (boolean) sendRequest(CMD_IS_NR_DUAL_CONNECTIVITY_ENABLED,
+                    null, subId, workSource);
+            if (DBG) log("isNRDualConnectivityEnabled: " + isEnabled);
+            return isEnabled;
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    /**
+     * get carrier bandwidth per primary and secondary carrier
+     * @param subId subscription id of the sim card
+     * @return CarrierBandwidth with bandwidth of both primary and secondary carrier..
+     */
+    @Override
+    public CarrierBandwidth getCarrierBandwidth(int subId) {
+        TelephonyPermissions
+                .enforeceCallingOrSelfReadPrivilegedPhoneStatePermissionOrCarrierPrivilege(
+                        mApp, subId, "isNRDualConnectivityEnabled");
+        WorkSource workSource = getWorkSource(Binder.getCallingUid());
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            CarrierBandwidth carrierBandwidth =
+                    getPhoneFromSubId(subId).getCarrierBandwidth();
+            if (DBG) log("getCarrierBandwidth: " + carrierBandwidth);
+            return carrierBandwidth;
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    /**
      * Get the effective allowed network types on the device.
      * This API will return an intersection of allowed network types for all reasons,
      * including the configuration done through setAllowedNetworkTypes
@@ -5680,33 +6014,6 @@
     }
 
     /**
-     * Set mobile data enabled
-     * Used by the user through settings etc to turn on/off mobile data
-     *
-     * @param enable {@code true} turn turn data on, else {@code false}
-     */
-    @Override
-    public void setUserDataEnabled(int subId, boolean enable) {
-        TelephonyPermissions.enforceCallingOrSelfModifyPermissionOrCarrierPrivilege(
-                mApp, subId, "setUserDataEnabled");
-
-        final long identity = Binder.clearCallingIdentity();
-        try {
-            int phoneId = mSubscriptionController.getPhoneId(subId);
-            if (DBG) log("setUserDataEnabled: subId=" + subId + " phoneId=" + phoneId);
-            Phone phone = PhoneFactory.getPhone(phoneId);
-            if (phone != null) {
-                if (DBG) log("setUserDataEnabled: subId=" + subId + " enable=" + enable);
-                phone.getDataEnabledSettings().setUserDataEnabled(enable);
-            } else {
-                loge("setUserDataEnabled: no phone found. Invalid subId=" + subId);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(identity);
-        }
-    }
-
-    /**
      * Enable or disable always reporting signal strength changes from radio.
      *
      * @param isEnable {@code true} for enabling; {@code false} for disabling.
@@ -5796,7 +6103,18 @@
      */
     @Override
     public boolean isDataEnabled(int subId) {
-        enforceReadPrivilegedPermission("isDataEnabled");
+        try {
+            try {
+                mApp.enforceCallingOrSelfPermission(
+                        android.Manifest.permission.ACCESS_NETWORK_STATE,
+                        null);
+            } catch (Exception e) {
+                mApp.enforceCallingOrSelfPermission(android.Manifest.permission.READ_PHONE_STATE,
+                        "isDataEnabled");
+            }
+        } catch (Exception e) {
+            enforceReadPrivilegedPermission("isDataEnabled");
+        }
 
         final long identity = Binder.clearCallingIdentity();
         try {
@@ -5816,6 +6134,53 @@
         }
     }
 
+    /**
+     * Check if data is enabled for a specific reason
+     * @param subId Subscription index
+     * @param reason the reason the data enable change is taking place
+     * @return {@code true} if the overall data is enabled; {@code false} if not.
+     */
+    @Override
+    public boolean isDataEnabledForReason(int subId,
+            @TelephonyManager.DataEnabledReason int reason) {
+        try {
+            mApp.enforceCallingOrSelfPermission(android.Manifest.permission.ACCESS_NETWORK_STATE,
+                    null);
+        } catch (Exception e) {
+            mApp.enforceCallingOrSelfPermission(android.Manifest.permission.READ_PHONE_STATE,
+                    "isDataEnabledForReason");
+        }
+
+
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            int phoneId = mSubscriptionController.getPhoneId(subId);
+            if (DBG) {
+                log("isDataEnabledForReason: subId=" + subId + " phoneId=" + phoneId
+                        + " reason=" + reason);
+            }
+            Phone phone = PhoneFactory.getPhone(phoneId);
+            if (phone != null) {
+                boolean retVal;
+                if (reason == TelephonyManager.DATA_ENABLED_REASON_USER) {
+                    retVal = phone.isUserDataEnabled();
+                } else {
+                    retVal = phone.getDataEnabledSettings().isDataEnabledForReason(reason);
+                }
+                if (DBG) log("isDataEnabledForReason: retVal=" + retVal);
+                return retVal;
+            } else {
+                if (DBG) {
+                    loge("isDataEnabledForReason: no phone subId="
+                            + subId + " retVal=false");
+                }
+                return false;
+            }
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
     private int getCarrierPrivilegeStatusFromCarrierConfigRules(int privilegeFromSim, int uid,
             Phone phone) {
         if (uid == Process.SYSTEM_UID || uid == Process.PHONE_UID) {
@@ -6020,12 +6385,10 @@
         final Phone phone = getPhone(subId);
         UiccCard card = phone == null ? null : phone.getUiccCard();
         if (card == null) {
-            loge("getIccId: No UICC");
             return null;
         }
         String iccId = card.getIccId();
         if (TextUtils.isEmpty(iccId)) {
-            loge("getIccId: ICC ID is null or empty.");
             return null;
         }
         return iccId;
@@ -6644,7 +7007,8 @@
         try {
             if (SubscriptionManager.isUsableSubIdValue(subId) && !mUserManager.hasUserRestriction(
                     UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS)) {
-                setUserDataEnabled(subId, getDefaultDataEnabled());
+                setDataEnabledForReason(subId, TelephonyManager.DATA_ENABLED_REASON_USER,
+                        getDefaultDataEnabled());
                 setNetworkSelectionModeAutomatic(subId);
                 setPreferredNetworkType(subId, getDefaultNetworkType(subId));
                 setDataRoamingEnabled(subId, getDefaultDataRoamingEnabled(subId));
@@ -7164,31 +7528,6 @@
     }
 
     /**
-     * Action set from carrier signalling broadcast receivers to enable/disable metered apns
-     * @param subId the subscription ID that this action applies to.
-     * @param enabled control enable or disable metered apns.
-     * {@hide}
-     */
-    @Override
-    public void carrierActionSetMeteredApnsEnabled(int subId, boolean enabled) {
-        enforceModifyPermission();
-        final Phone phone = getPhone(subId);
-
-        final long identity = Binder.clearCallingIdentity();
-        if (phone == null) {
-            loge("carrierAction: SetMeteredApnsEnabled fails with invalid subId: " + subId);
-            return;
-        }
-        try {
-            phone.carrierActionSetMeteredApnsEnabled(enabled);
-        } catch (Exception e) {
-            Log.e(LOG_TAG, "carrierAction: SetMeteredApnsEnabled fails. Exception ex=" + e);
-        } finally {
-            Binder.restoreCallingIdentity(identity);
-        }
-    }
-
-    /**
      * Action set from carrier signalling broadcast receivers to enable/disable radio
      * @param subId the subscription ID that this action applies to.
      * @param enabled control enable or disable radio.
@@ -7289,20 +7628,36 @@
     }
 
     /**
-     * Policy control of data connection. Usually used when data limit is passed.
-     * @param enabled True if enabling the data, otherwise disabling.
+     * Policy control of data connection with reason {@@TelephonyManager.DataEnabledReason}
      * @param subId Subscription index
-     * {@hide}
+     * @param reason the reason the data enable change is taking place
+     * @param enabled True if enabling the data, otherwise disabling.
+     * @hide
      */
     @Override
-    public void setPolicyDataEnabled(boolean enabled, int subId) {
-        enforceModifyPermission();
+    public void setDataEnabledForReason(int subId, @TelephonyManager.DataEnabledReason int reason,
+            boolean enabled) {
+        if (reason == TelephonyManager.DATA_ENABLED_REASON_USER
+                || reason == TelephonyManager.DATA_ENABLED_REASON_CARRIER) {
+            try {
+                TelephonyPermissions.enforceCallingOrSelfCarrierPrivilege(
+                        mApp, subId, "setDataEnabledForReason");
+            } catch (SecurityException se) {
+                enforceModifyPermission();
+            }
+        } else {
+            enforceModifyPermission();
+        }
 
         final long identity = Binder.clearCallingIdentity();
         try {
             Phone phone = getPhone(subId);
             if (phone != null) {
-                phone.getDataEnabledSettings().setPolicyDataEnabled(enabled);
+                if (reason == TelephonyManager.DATA_ENABLED_REASON_CARRIER) {
+                    phone.carrierActionSetMeteredApnsEnabled(enabled);
+                } else {
+                    phone.getDataEnabledSettings().setDataEnabled(reason, enabled);
+                }
             }
         } finally {
             Binder.restoreCallingIdentity(identity);
@@ -7825,6 +8180,20 @@
     }
 
     @Override
+    public int getCdmaSubscriptionMode(int subId) {
+        TelephonyPermissions
+                .enforeceCallingOrSelfReadPrivilegedPhoneStatePermissionOrCarrierPrivilege(
+                        mApp, subId, "getCdmaSubscriptionMode");
+
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            return (int) sendRequest(CMD_GET_CDMA_SUBSCRIPTION_MODE, null /* argument */, subId);
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    @Override
     public boolean setCdmaSubscriptionMode(int subId, int mode) {
         TelephonyPermissions.enforceCallingOrSelfModifyPermissionOrCarrierPrivilege(
                 mApp, subId, "setCdmaSubscriptionMode");
@@ -8257,7 +8626,7 @@
      *  1) user data is turned on, or
      *  2) APN is un-metered for this subscription, or
      *  3) APN type is whitelisted. E.g. MMS is whitelisted if
-     *  {@link TelephonyManager#setAlwaysAllowMmsData} is turned on.
+     *  {@link TelephonyManager#MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED} is enabled.
      *
      * @return whether data is allowed for a apn type.
      *
@@ -8399,55 +8768,54 @@
     }
 
     @Override
-    public boolean setDataAllowedDuringVoiceCall(int subId, boolean allow) {
-        enforceModifyPermission();
+    public boolean isMobileDataPolicyEnabled(int subscriptionId, int policy) {
+        enforceReadPrivilegedPermission("isMobileDataPolicyEnabled");
 
-        // Now that all security checks passes, perform the operation as ourselves.
         final long identity = Binder.clearCallingIdentity();
         try {
-            Phone phone = getPhone(subId);
+            Phone phone = getPhone(subscriptionId);
             if (phone == null) return false;
 
-            return phone.getDataEnabledSettings().setAllowDataDuringVoiceCall(allow);
+            switch (policy) {
+                case TelephonyManager.MOBILE_DATA_POLICY_DATA_ON_NON_DEFAULT_DURING_VOICE_CALL:
+                    return phone.getDataEnabledSettings().isDataAllowedInVoiceCall();
+                case TelephonyManager.MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED:
+                    return phone.getDataEnabledSettings().isMmsAlwaysAllowed();
+                default:
+                    throw new IllegalArgumentException(policy + " is not a valid policy");
+            }
         } finally {
             Binder.restoreCallingIdentity(identity);
         }
     }
 
     @Override
-    public boolean isDataAllowedInVoiceCall(int subId) {
-        enforceReadPrivilegedPermission("isDataAllowedInVoiceCall");
-
-        // Now that all security checks passes, perform the operation as ourselves.
-        final long identity = Binder.clearCallingIdentity();
-        try {
-            Phone phone = getPhone(subId);
-            if (phone == null) return false;
-
-            return phone.getDataEnabledSettings().isDataAllowedInVoiceCall();
-        } finally {
-            Binder.restoreCallingIdentity(identity);
-        }
-    }
-
-    @Override
-    public boolean setAlwaysAllowMmsData(int subId, boolean alwaysAllow) {
+    public void setMobileDataPolicyEnabledStatus(int subscriptionId, int policy,
+            boolean enabled) {
         enforceModifyPermission();
 
-        // Now that all security checks passes, perform the operation as ourselves.
         final long identity = Binder.clearCallingIdentity();
         try {
-            Phone phone = getPhone(subId);
-            if (phone == null) return false;
+            Phone phone = getPhone(subscriptionId);
+            if (phone == null) return;
 
-            return phone.getDataEnabledSettings().setAlwaysAllowMmsData(alwaysAllow);
+            switch (policy) {
+                case TelephonyManager.MOBILE_DATA_POLICY_DATA_ON_NON_DEFAULT_DURING_VOICE_CALL:
+                    phone.getDataEnabledSettings().setAllowDataDuringVoiceCall(enabled);
+                    break;
+                case TelephonyManager.MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED:
+                    phone.getDataEnabledSettings().setAlwaysAllowMmsData(enabled);
+                    break;
+                default:
+                    throw new IllegalArgumentException(policy + " is not a valid policy");
+            }
         } finally {
             Binder.restoreCallingIdentity(identity);
         }
     }
 
     /**
-     * Updates whether conference event pacakge handling is enabled.
+     * Updates whether conference event package handling is enabled.
      * @param isCepEnabled {@code true} if CEP handling is enabled (default), or {@code false}
      *                                 otherwise.
      */
diff --git a/src/com/android/phone/TelephonyShellCommand.java b/src/com/android/phone/TelephonyShellCommand.java
index c3f2974..33d0721 100644
--- a/src/com/android/phone/TelephonyShellCommand.java
+++ b/src/com/android/phone/TelephonyShellCommand.java
@@ -17,7 +17,6 @@
 package com.android.phone;
 
 import android.content.Context;
-import android.os.BasicShellCommandHandler;
 import android.os.Binder;
 import android.os.PersistableBundle;
 import android.os.Process;
@@ -34,6 +33,7 @@
 import com.android.internal.telephony.PhoneFactory;
 import com.android.internal.telephony.emergency.EmergencyNumberTracker;
 import com.android.internal.telephony.util.TelephonyUtils;
+import com.android.modules.utils.BasicShellCommandHandler;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -64,8 +64,9 @@
     private static final String DATA_ENABLE = "enable";
     private static final String DATA_DISABLE = "disable";
 
-    private static final String IMS_SET_CARRIER_SERVICE = "set-ims-service";
-    private static final String IMS_GET_CARRIER_SERVICE = "get-ims-service";
+    private static final String IMS_SET_IMS_SERVICE = "set-ims-service";
+    private static final String IMS_GET_IMS_SERVICE = "get-ims-service";
+    private static final String IMS_CLEAR_SERVICE_OVERRIDE = "clear-ims-service-override";
     private static final String IMS_ENABLE = "enable";
     private static final String IMS_DISABLE = "disable";
     // Used to disable or enable processing of conference event package data from the network.
@@ -210,6 +211,11 @@
         pw.println("      -d: The ImsService defined as the device default ImsService.");
         pw.println("      -f: The feature type that the query will be requested for. If none is");
         pw.println("          specified, the returned package name will correspond to MMTEL.");
+        pw.println("  ims clear-ims-service-override [-s SLOT_ID]");
+        pw.println("    Clear all carrier ImsService overrides. This does not work for device ");
+        pw.println("    configuration overrides. Options are:");
+        pw.println("      -s: The SIM slot ID for the registered ImsService. If no option");
+        pw.println("          is specified, it will choose the default voice SIM slot.");
         pw.println("  ims enable [-s SLOT_ID]");
         pw.println("    enables IMS for the SIM slot specified, or for the default voice SIM slot");
         pw.println("    if none is specified.");
@@ -295,12 +301,15 @@
         }
 
         switch (arg) {
-            case IMS_SET_CARRIER_SERVICE: {
+            case IMS_SET_IMS_SERVICE: {
                 return handleImsSetServiceCommand();
             }
-            case IMS_GET_CARRIER_SERVICE: {
+            case IMS_GET_IMS_SERVICE: {
                 return handleImsGetServiceCommand();
             }
+            case IMS_CLEAR_SERVICE_OVERRIDE: {
+                return handleImsClearCarrierServiceCommand();
+            }
             case IMS_ENABLE: {
                 return handleEnableIms();
             }
@@ -547,6 +556,42 @@
         return 0;
     }
 
+    // ims clear-ims-service-override
+    private int handleImsClearCarrierServiceCommand() {
+        PrintWriter errPw = getErrPrintWriter();
+        int slotId = getDefaultSlot();
+
+        String opt;
+        while ((opt = getNextOption()) != null) {
+            switch (opt) {
+                case "-s": {
+                    try {
+                        slotId = Integer.parseInt(getNextArgRequired());
+                    } catch (NumberFormatException e) {
+                        errPw.println("ims set-ims-service requires an integer as a SLOT_ID.");
+                        return -1;
+                    }
+                    break;
+                }
+            }
+        }
+
+        try {
+            boolean result = mInterface.clearCarrierImsServiceOverride(slotId);
+            if (VDBG) {
+                Log.v(LOG_TAG, "ims clear-ims-service-override -s " + slotId
+                        + ", result=" + result);
+            }
+            getOutPrintWriter().println(result);
+        } catch (RemoteException e) {
+            Log.w(LOG_TAG, "ims clear-ims-service-override -s " + slotId
+                    + ", error" + e.getMessage());
+            errPw.println("Exception: " + e.getMessage());
+            return -1;
+        }
+        return 0;
+    }
+
     // ims get-ims-service
     private int handleImsGetServiceCommand() {
         PrintWriter errPw = getErrPrintWriter();
diff --git a/src/com/android/services/telephony/ImsConference.java b/src/com/android/services/telephony/ImsConference.java
index 4826ecb..c9f762b 100644
--- a/src/com/android/services/telephony/ImsConference.java
+++ b/src/com/android/services/telephony/ImsConference.java
@@ -16,6 +16,7 @@
 
 package com.android.services.telephony;
 
+import android.annotation.NonNull;
 import android.content.Context;
 import android.graphics.drawable.Icon;
 import android.net.Uri;
@@ -28,6 +29,7 @@
 import android.telecom.TelecomManager;
 import android.telecom.VideoProfile;
 import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
 import android.util.Pair;
 
 import com.android.ims.internal.ConferenceParticipant;
@@ -1269,58 +1271,79 @@
     }
 
     /**
+     * Extracts a phone number from a {@link Uri}.
+     * <p>
+     * Phone numbers can be represented either as a TEL URI or a SIP URI.
+     * For conference event packages, RFC3261 specifies how participants can be identified using a
+     * SIP URI.
+     * A valid SIP uri has the format: sip:user:password@host:port;uri-parameters?headers
+     * Per RFC3261, the "user" can be a telephone number.
+     * For example: sip:1650555121;phone-context=blah.com@host.com
+     * In this case, the phone number is in the user field of the URI, and the parameters can be
+     * ignored.
+     *
+     * A SIP URI can also specify a phone number in a format similar to:
+     * sip:+1-212-555-1212@something.com;user=phone
+     * In this case, the phone number is again in user field and the parameters can be ignored.
+     * We can get the user field in these instances by splitting the string on the @, ;, or :
+     * and looking at the first found item.
+     * @param handle The URI containing a SIP or TEL formatted phone number.
+     * @return extracted phone number.
+     */
+    private static @NonNull String extractPhoneNumber(@NonNull Uri handle) {
+        // Number is always in the scheme specific part, regardless of whether this is a TEL or SIP
+        // URI.
+        String number = handle.getSchemeSpecificPart();
+        // Get anything before the @ for the SIP case.
+        String[] numberParts = number.split("[@;:]");
+
+        if (numberParts.length == 0) {
+            Log.v(LOG_TAG, "extractPhoneNumber(N) : no number in handle");
+            return "";
+        }
+        return numberParts[0];
+    }
+
+    /**
      * Determines if the passed in participant handle is the same as the conference host's handle.
      * Starts with a simple equality check.  However, the handles from a conference event package
      * will be a SIP uri, so we need to pull that apart to look for the participant's phone number.
      *
-     * @param hostHandles The handle(s) of the connection hosting the conference.
+     * @param hostHandles The handle(s) of the connection hosting the conference, typically obtained
+     *                    from P-Associated-Uri entries.
      * @param handle The handle of the conference participant.
      * @return {@code true} if the host's handle matches the participant's handle, {@code false}
      *      otherwise.
      */
-    private boolean isParticipantHost(Uri[] hostHandles, Uri handle) {
+    @VisibleForTesting
+    public static boolean isParticipantHost(Uri[] hostHandles, Uri handle) {
         // If there is no host handle or no participant handle, bail early.
         if (hostHandles == null || hostHandles.length == 0 || handle == null) {
-            Log.v(this, "isParticipantHost(N) : host or participant uri null");
+            Log.v(LOG_TAG, "isParticipantHost(N) : host or participant uri null");
             return false;
         }
 
-        // Conference event package participants are identified using SIP URIs (see RFC3261).
-        // A valid SIP uri has the format: sip:user:password@host:port;uri-parameters?headers
-        // Per RFC3261, the "user" can be a telephone number.
-        // For example: sip:1650555121;phone-context=blah.com@host.com
-        // In this case, the phone number is in the user field of the URI, and the parameters can be
-        // ignored.
-        //
-        // A SIP URI can also specify a phone number in a format similar to:
-        // sip:+1-212-555-1212@something.com;user=phone
-        // In this case, the phone number is again in user field and the parameters can be ignored.
-        // We can get the user field in these instances by splitting the string on the @, ;, or :
-        // and looking at the first found item.
-
-        String number = handle.getSchemeSpecificPart();
-        String numberParts[] = number.split("[@;:]");
-
-        if (numberParts.length == 0) {
-            Log.v(this, "isParticipantHost(N) : no number in participant handle");
+        String number = extractPhoneNumber(handle);
+        // If we couldn't extract the participant's number, then we can't determine if it is the
+        // host or not.
+        if (TextUtils.isEmpty(number)) {
             return false;
         }
-        number = numberParts[0];
 
         for (Uri hostHandle : hostHandles) {
             if (hostHandle == null) {
                 continue;
             }
-            // The host number will be a tel: uri.  Per RFC3966, the part after tel: is the phone
-            // number.
-            String hostNumber = hostHandle.getSchemeSpecificPart();
+            // Similar to the CEP participant data, the host identity in the P-Associated-Uri could
+            // be a SIP URI or a TEL URI.
+            String hostNumber = extractPhoneNumber(hostHandle);
 
             // Use a loose comparison of the phone numbers.  This ensures that numbers that differ
             // by special characters are counted as equal.
             // E.g. +16505551212 would be the same as 16505551212
             boolean isHost = PhoneNumberUtils.compare(hostNumber, number);
 
-            Log.v(this, "isParticipantHost(%s) : host: %s, participant %s", (isHost ? "Y" : "N"),
+            Log.v(LOG_TAG, "isParticipantHost(%s) : host: %s, participant %s", (isHost ? "Y" : "N"),
                     Rlog.pii(LOG_TAG, hostNumber), Rlog.pii(LOG_TAG, number));
 
             if (isHost) {
diff --git a/src/com/android/services/telephony/TelecomAccountRegistry.java b/src/com/android/services/telephony/TelecomAccountRegistry.java
index 46c245a..0e7c8ed 100644
--- a/src/com/android/services/telephony/TelecomAccountRegistry.java
+++ b/src/com/android/services/telephony/TelecomAccountRegistry.java
@@ -428,8 +428,7 @@
                             .getBoolean(R.bool.config_support_video_calling_fallback));
 
             if (slotId != SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
-                extras.putString(PhoneAccount.EXTRA_SORT_ORDER,
-                    String.valueOf(slotId));
+                extras.putInt(PhoneAccount.EXTRA_SORT_ORDER, slotId);
             }
 
             mIsMergeCallSupported = isCarrierMergeCallSupported();
diff --git a/src/com/android/services/telephony/TelephonyConnection.java b/src/com/android/services/telephony/TelephonyConnection.java
index 1caa8e7..09c83c0 100755
--- a/src/com/android/services/telephony/TelephonyConnection.java
+++ b/src/com/android/services/telephony/TelephonyConnection.java
@@ -96,6 +96,9 @@
     private static final int MSG_CONFERENCE_MERGE_FAILED = 6;
     private static final int MSG_SUPP_SERVICE_NOTIFY = 7;
 
+    // the threshold used to compare mAudioCodecBitrateKbps and mAudioCodecBandwidth.
+    private static final float THRESHOLD = 0.01f;
+
     /**
      * Mappings from {@link com.android.internal.telephony.Connection} extras keys to their
      * equivalents defined in {@link android.telecom.Connection}.
@@ -1496,7 +1499,8 @@
         }
     }
 
-    private void refreshCodecType() {
+    private void refreshCodec() {
+        boolean changed = false;
         Bundle newExtras = getExtras();
         if (newExtras == null) {
             newExtras = new Bundle();
@@ -1512,6 +1516,31 @@
                 Connection.AUDIO_CODEC_NONE);
         if (newCodecType != oldCodecType) {
             newExtras.putInt(Connection.EXTRA_AUDIO_CODEC, newCodecType);
+            changed = true;
+        }
+        if (isImsConnection()) {
+            float newBitrate = getOriginalConnection().getAudioCodecBitrateKbps();
+            float oldBitrate = newExtras.getFloat(Connection.EXTRA_AUDIO_CODEC_BITRATE_KBPS, 0.0f);
+            if (Math.abs(newBitrate - oldBitrate) > THRESHOLD) {
+                newExtras.putFloat(Connection.EXTRA_AUDIO_CODEC_BITRATE_KBPS, newBitrate);
+                changed = true;
+            }
+
+            float newBandwidth = getOriginalConnection().getAudioCodecBandwidthKhz();
+            float oldBandwidth = newExtras.getFloat(Connection.EXTRA_AUDIO_CODEC_BANDWIDTH_KHZ,
+                    0.0f);
+            if (Math.abs(newBandwidth - oldBandwidth) > THRESHOLD) {
+                newExtras.putFloat(Connection.EXTRA_AUDIO_CODEC_BANDWIDTH_KHZ, newBandwidth);
+                changed = true;
+            }
+        } else {
+            ArrayList<String> toRemove = new ArrayList<>();
+            toRemove.add(Connection.EXTRA_AUDIO_CODEC_BITRATE_KBPS);
+            toRemove.add(Connection.EXTRA_AUDIO_CODEC_BANDWIDTH_KHZ);
+            removeTelephonyExtras(toRemove);
+        }
+
+        if (changed) {
             putTelephonyExtras(newExtras);
         }
     }
@@ -2195,7 +2224,7 @@
         updateAddress();
         updateMultiparty();
         refreshDisableAddCall();
-        refreshCodecType();
+        refreshCodec();
     }
 
     /**
diff --git a/src/com/android/services/telephony/TelephonyConnectionService.java b/src/com/android/services/telephony/TelephonyConnectionService.java
index 953d415..322993a 100644
--- a/src/com/android/services/telephony/TelephonyConnectionService.java
+++ b/src/com/android/services/telephony/TelephonyConnectionService.java
@@ -1687,7 +1687,6 @@
                 EmergencyNumber emergencyNumber =
                         phone.getEmergencyNumberTracker().getEmergencyNumber(number);
                 if (emergencyNumber != null) {
-                    phone.notifyOutgoingEmergencyCall(emergencyNumber);
                     if (!getAllConnections().isEmpty()) {
                         if (!shouldHoldForEmergencyCall(phone)) {
                             // If we do not support holding ongoing calls for an outgoing
diff --git a/src/com/android/services/telephony/rcs/DelegateBinderStateManager.java b/src/com/android/services/telephony/rcs/DelegateBinderStateManager.java
new file mode 100644
index 0000000..39e9965
--- /dev/null
+++ b/src/com/android/services/telephony/rcs/DelegateBinderStateManager.java
@@ -0,0 +1,92 @@
+/*
+ * 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.telephony.ims.DelegateRegistrationState;
+import android.telephony.ims.DelegateRequest;
+import android.telephony.ims.FeatureTagState;
+import android.telephony.ims.SipDelegateImsConfiguration;
+import android.telephony.ims.aidl.ISipDelegate;
+import android.telephony.ims.aidl.ISipDelegateMessageCallback;
+import android.telephony.ims.aidl.ISipTransport;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+/**
+ * Defines the interface to be used to manage the state of a SipDelegate on the ImsService side.
+ */
+public interface DelegateBinderStateManager {
+
+    /**
+     * Callback interface that allows listeners to listen to changes in registration or
+     * configuration state.
+     */
+    interface StateCallback {
+        /**
+         * The SipDelegate has notified telephony that the registration state has changed.
+         */
+        void onRegistrationStateChanged(DelegateRegistrationState registrationState);
+
+        /**
+         * The SipDelegate has notified telephony that the IMS configuration has changed.
+         */
+        void onImsConfigurationChanged(SipDelegateImsConfiguration config);
+    }
+
+    /** Allow for mocks to be created for testing. */
+    @VisibleForTesting
+    interface Factory {
+        /**
+         * Create a new instance of this interface, which may change depending on the tags being
+         * denied. See {@link SipDelegateBinderConnectionStub} and
+         * {@link SipDelegateBinderConnection}
+         */
+        DelegateBinderStateManager create(int subId, ISipTransport sipTransport,
+                DelegateRequest requestedConfig, Set<FeatureTagState> transportDeniedTags,
+                Executor executor, List<StateCallback> stateCallbacks);
+    }
+
+    /**
+     * Start the process to create a SipDelegate on the ImsService.
+     * @param cb The Binder interface that the SipDelegate should use to notify new incoming SIP
+     *         messages as well as acknowledge whether or not an outgoing SIP message was
+     *         successfully sent.
+     * @param createdConsumer The consumer that will be notified when the creation process has
+     *         completed. Contains the ISipDelegate interface to communicate with the SipDelegate
+     *         and the feature tags the SipDelegate itself denied.
+     * @return true if the creation process started, false if the remote process died. If false, the
+     * consumers will not be notified.
+     */
+    boolean create(ISipDelegateMessageCallback cb,
+            BiConsumer<ISipDelegate, Set<FeatureTagState>> createdConsumer);
+
+    /**
+     * Destroy the existing SipDelegate managed by this object.
+     * <p>
+     * This instance should be cleaned up after this call.
+     * @param reason The reason for why this delegate is being destroyed.
+     * @param destroyedConsumer The consumer that will be notified when this operation completes.
+     *         Contains the reason the SipDelegate reported it was destroyed.
+     */
+    void destroy(int reason, Consumer<Integer> destroyedConsumer);
+}
diff --git a/src/com/android/services/telephony/rcs/DelegateStateTracker.java b/src/com/android/services/telephony/rcs/DelegateStateTracker.java
new file mode 100644
index 0000000..1d8fa3b
--- /dev/null
+++ b/src/com/android/services/telephony/rcs/DelegateStateTracker.java
@@ -0,0 +1,207 @@
+/*
+ * 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.os.RemoteException;
+import android.telephony.ims.DelegateRegistrationState;
+import android.telephony.ims.FeatureTagState;
+import android.telephony.ims.SipDelegateImsConfiguration;
+import android.telephony.ims.aidl.ISipDelegate;
+import android.telephony.ims.aidl.ISipDelegateConnectionStateCallback;
+import android.telephony.ims.stub.DelegateConnectionStateCallback;
+import android.util.LocalLog;
+import android.util.Log;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Manages the events sent back to the remote IMS application using the AIDL backing for the
+ * {@link DelegateConnectionStateCallback} interface.
+ */
+public class DelegateStateTracker implements DelegateBinderStateManager.StateCallback {
+    private static final String LOG_TAG = "DelegateST";
+
+    private final int mSubId;
+    private final ISipDelegateConnectionStateCallback mAppStateCallback;
+    private final ISipDelegate mLocalDelegateImpl;
+
+    private final LocalLog mLocalLog = new LocalLog(SipTransportController.LOG_SIZE);
+
+    private List<FeatureTagState> mDelegateDeniedTags;
+    private DelegateRegistrationState mLastRegState;
+    private boolean mCreatedCalled = false;
+    private int mRegistrationStateOverride = -1;
+
+    public DelegateStateTracker(int subId, ISipDelegateConnectionStateCallback appStateCallback,
+            ISipDelegate localDelegateImpl) {
+        mSubId = subId;
+        mAppStateCallback = appStateCallback;
+        mLocalDelegateImpl = localDelegateImpl;
+    }
+
+    /**
+     * Notify this state tracker that a new internal SipDelegate has been connected.
+     *
+     * Registration and state updates will be send via the
+     * {@link SipDelegateBinderConnection.StateCallback} callback implemented by this class as they
+     * arrive.
+     * @param deniedTags The tags denied by the SipTransportController and ImsService creating the
+     *         SipDelegate. These tags will need to be notified back to the IMS application.
+     */
+    public void sipDelegateConnected(Set<FeatureTagState> deniedTags) {
+        logi("SipDelegate connected with denied tags:" + deniedTags);
+        // From the IMS application perspective, we only call onCreated/onDestroyed once and
+        // provide the local implementation of ISipDelegate, which doesn't change, even though
+        // SipDelegates may be changing underneath.
+        if (!mCreatedCalled) {
+            mCreatedCalled = true;
+            notifySipDelegateCreated();
+        }
+        mRegistrationStateOverride = -1;
+        mDelegateDeniedTags = new ArrayList<>(deniedTags);
+    }
+
+    /**
+     * The underlying SipDelegate is changing due to a state change in the SipDelegateController.
+     *
+     * This will trigger an override of the IMS application's registration state. All feature tags
+     * in the REGISTERED state will be overridden to move to the deregistering state specified until
+     * a new SipDelegate was successfully created and {@link #sipDelegateConnected(Set)} was called
+     * or it was destroyed and {@link #sipDelegateDestroyed(int)} was called.
+     * @param deregisteringReason The new deregistering reason that all feature tags in the
+     *         registered state should now report.
+     */
+    public void sipDelegateChanging(int deregisteringReason) {
+        logi("SipDelegate Changing");
+        mRegistrationStateOverride = deregisteringReason;
+        if (mLastRegState == null) {
+            logw("sipDelegateChanging: invalid state, onRegistrationStateChanged never called.");
+            mLastRegState = new DelegateRegistrationState.Builder().build();
+        }
+        onRegistrationStateChanged(mLastRegState);
+    }
+
+    /**
+     * The underlying SipDelegate has been destroyed.
+     *
+     * This should only be called when the entire {@link SipDelegateController} is going down
+     * because the application has requested that the SipDelegate be destroyed.
+     *
+     * This can also be called in error conditions where the IMS application or ImsService has
+     * crashed.
+     * @param reason The reason that will be sent to the IMS application for why the SipDelegate
+     *         is being destroyed.
+     */
+    public void sipDelegateDestroyed(int reason) {
+        logi("SipDelegate destroyed:" + reason);
+        mRegistrationStateOverride = -1;
+        try {
+            mAppStateCallback.onDestroyed(reason);
+        } catch (RemoteException e) {
+            logw("sipDelegateDestroyed: IMS application is dead: " + e);
+        }
+    }
+
+    /**
+     * The underlying SipDelegate has reported that its registration state has changed.
+     * @param registrationState The RegistrationState reported by the SipDelegate to be sent to the
+     *         IMS application.
+     */
+    @Override
+    public void onRegistrationStateChanged(DelegateRegistrationState registrationState) {
+        if (mRegistrationStateOverride > DelegateRegistrationState.DEREGISTERED_REASON_UNKNOWN) {
+            logi("onRegistrationStateChanged: overriding registered state to "
+                    + mRegistrationStateOverride);
+            registrationState = overrideRegistrationForDelegateChange(mRegistrationStateOverride,
+                    registrationState);
+        }
+        if (registrationState.equals(mLastRegState)) {
+            logi("onRegistrationStateChanged: skipping notification, state is the same.");
+            return;
+        }
+        mLastRegState = registrationState;
+        logi("onRegistrationStateChanged: sending reg state " + registrationState);
+        try {
+            mAppStateCallback.onFeatureTagStatusChanged(registrationState, mDelegateDeniedTags);
+        } catch (RemoteException e) {
+            logw("onRegistrationStateChanged: IMS application is dead: " + e);
+        }
+    }
+
+    /**
+     * THe underlying SipDelegate has reported that the IMS configuration has changed.
+     * @param config The config to be sent to the IMS application.
+     */
+    @Override
+    public void onImsConfigurationChanged(SipDelegateImsConfiguration config) {
+        logi("onImsConfigurationChanged: Sending new IMS configuration.");
+        try {
+            mAppStateCallback.onImsConfigurationChanged(config);
+        } catch (RemoteException e) {
+            logw("onImsConfigurationChanged: IMS application is dead: " + e);
+        }
+    }
+
+    /** Write state about this tracker into the PrintWriter to be included in the dumpsys */
+    public void dump(PrintWriter printWriter) {
+        printWriter.println("Last reg state: " + mLastRegState);
+        printWriter.println("Denied tags: " + mDelegateDeniedTags);
+        printWriter.println("Most recent logs: ");
+        printWriter.println();
+        mLocalLog.dump(printWriter);
+    }
+
+    private DelegateRegistrationState overrideRegistrationForDelegateChange(
+            int registerOverrideReason, DelegateRegistrationState state) {
+        Set<String> registeredFeatures = state.getRegisteredFeatureTags();
+        DelegateRegistrationState.Builder overriddenState = new DelegateRegistrationState.Builder();
+        // keep other deregistering/deregistered tags the same.
+        for (FeatureTagState dereging : state.getDeregisteringFeatureTags()) {
+            overriddenState.addDeregisteringFeatureTag(dereging.getFeatureTag(),
+                    dereging.getState());
+        }
+        for (FeatureTagState dereged : state.getDeregisteredFeatureTags()) {
+            overriddenState.addDeregisteredFeatureTag(dereged.getFeatureTag(),
+                    dereged.getState());
+        }
+        // Override REGISTERED only
+        for (String ft : registeredFeatures) {
+            overriddenState.addDeregisteringFeatureTag(ft, registerOverrideReason);
+        }
+        return overriddenState.build();
+    }
+
+    private void notifySipDelegateCreated() {
+        try {
+            mAppStateCallback.onCreated(mLocalDelegateImpl);
+        } catch (RemoteException e) {
+            logw("notifySipDelegateCreated: IMS application is dead: " + e);
+        }
+    }
+
+    private void logi(String log) {
+        Log.i(SipTransportController.LOG_TAG, LOG_TAG + "[" + mSubId + "] " + log);
+        mLocalLog.log("[I] " + log);
+    }
+    private void logw(String log) {
+        Log.w(SipTransportController.LOG_TAG, LOG_TAG + "[" + mSubId + "] " + log);
+        mLocalLog.log("[W] " + log);
+    }
+}
diff --git a/src/com/android/services/telephony/rcs/MessageTransportStateTracker.java b/src/com/android/services/telephony/rcs/MessageTransportStateTracker.java
new file mode 100644
index 0000000..0691ae5
--- /dev/null
+++ b/src/com/android/services/telephony/rcs/MessageTransportStateTracker.java
@@ -0,0 +1,478 @@
+/*
+ * 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.os.Binder;
+import android.os.RemoteException;
+import android.telephony.ims.DelegateMessageCallback;
+import android.telephony.ims.DelegateRegistrationState;
+import android.telephony.ims.FeatureTagState;
+import android.telephony.ims.SipDelegateImsConfiguration;
+import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.SipMessage;
+import android.telephony.ims.aidl.ISipDelegate;
+import android.telephony.ims.aidl.ISipDelegateMessageCallback;
+import android.telephony.ims.stub.SipDelegate;
+import android.util.LocalLog;
+import android.util.Log;
+
+import java.io.PrintWriter;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * Tracks the SIP message path both from the IMS application to the SipDelegate and from the
+ * SipDelegate back to the IMS Application.
+ * <p>
+ * Responsibilities include:
+ * 1) Queue incoming and outgoing SIP messages and deliver to IMS application and SipDelegate in
+ *        order. If there is an error delivering the message, notify the caller.
+ * 2) TODO Perform basic validation of outgoing messages.
+ * 3) TODO Record the status of ongoing SIP Dialogs and trigger the completion of pending
+ *         consumers when they are finished or call closeDialog to clean up the SIP
+ *         dialogs that did not complete within the allotted timeout time.
+ * <p>
+ * Note: This handles incoming binder calls, so all calls from other processes should be handled on
+ * the provided Executor.
+ */
+public class MessageTransportStateTracker implements DelegateBinderStateManager.StateCallback {
+    private static final String TAG = "MessageST";
+
+    /**
+     * Communicates the result of verifying whether a SIP message should be sent based on the
+     * contents of the SIP message as well as if the transport is in an available state for the
+     * intended recipient of the message.
+     */
+    private static class VerificationResult {
+        public static final VerificationResult SUCCESS = new VerificationResult();
+
+        /**
+         * If {@code true}, the requested SIP message has been verified to be sent to the remote. If
+         * {@code false}, the SIP message has failed verification and should not be sent to the
+         * result. The {@link #restrictedReason} field will contain the reason for the verification
+         * failure.
+         */
+        public final boolean isVerified;
+
+        /**
+         * The reason associated with why the SIP message was not verified and generated a
+         * {@code false} result for {@link #isVerified}.
+         */
+        public final int restrictedReason;
+
+        /**
+         * Communicates a verified result of success. Use {@link #SUCCESS} instead.
+         */
+        private VerificationResult() {
+            isVerified = true;
+            restrictedReason = SipDelegateManager.MESSAGE_FAILURE_REASON_UNKNOWN;
+        }
+
+        /**
+         * The result of verifying that the SIP Message should be sent.
+         * @param reason The reason associated with why the SIP message was not verified and
+         *               generated a {@code false} result for {@link #isVerified}.
+         */
+        VerificationResult(@SipDelegateManager.MessageFailureReason int reason) {
+            isVerified = false;
+            restrictedReason = reason;
+        }
+    }
+
+    // SipDelegateConnection(IMS Application) -> SipDelegate(ImsService)
+    private final ISipDelegate.Stub mSipDelegateConnection = new ISipDelegate.Stub() {
+        /**
+         * The IMS application is acknowledging that it has successfully received and processed an
+         * incoming SIP message sent by the SipDelegate in
+         * {@link ISipDelegateMessageCallback#onMessageReceived(SipMessage)}.
+         */
+        @Override
+        public void notifyMessageReceived(String viaTransactionId) {
+            long token = Binder.clearCallingIdentity();
+            try {
+                mExecutor.execute(() -> {
+                    if (mSipDelegate == null) {
+                        logw("notifyMessageReceived called when SipDelegate is not associated for "
+                                + "transaction id: " + viaTransactionId);
+                        return;
+                    }
+                    try {
+                        // TODO track the SIP Dialogs created/destroyed on the associated
+                        // SipDelegate.
+                        mSipDelegate.notifyMessageReceived(viaTransactionId);
+                    } catch (RemoteException e) {
+                        logw("SipDelegate not available when notifyMessageReceived was called "
+                                + "for transaction id: " + viaTransactionId);
+                    }
+                });
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        /**
+         * The IMS application is acknowledging that it received an incoming SIP message sent by the
+         * SipDelegate in {@link ISipDelegateMessageCallback#onMessageReceived(SipMessage)} but it
+         * was unable to process it.
+         */
+        @Override
+        public void notifyMessageReceiveError(String viaTransactionId, int reason) {
+            long token = Binder.clearCallingIdentity();
+            try {
+                mExecutor.execute(() -> {
+                    if (mSipDelegate == null) {
+                        logw("notifyMessageReceiveError called when SipDelegate is not associated "
+                                + "for transaction id: " + viaTransactionId);
+                        return;
+                    }
+                    try {
+                        // TODO track the SIP Dialogs created/destroyed on the associated
+                        // SipDelegate.
+                        mSipDelegate.notifyMessageReceiveError(viaTransactionId, reason);
+                    } catch (RemoteException e) {
+                        logw("SipDelegate not available when notifyMessageReceiveError was called "
+                                + "for transaction id: " + viaTransactionId);
+                    }
+                });
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        /**
+         * The IMS application is sending an outgoing SIP message to the SipDelegate to be processed
+         * and sent over the network.
+         */
+        @Override
+        public void sendMessage(SipMessage sipMessage, long configVersion) {
+            long token = Binder.clearCallingIdentity();
+            try {
+                mExecutor.execute(() -> {
+                    VerificationResult result = verifyOutgoingMessage(sipMessage);
+                    if (!result.isVerified) {
+                        notifyDelegateSendError("Outgoing messages restricted", sipMessage,
+                                result.restrictedReason);
+                        return;
+                    }
+                    try {
+                        // TODO track the SIP Dialogs created/destroyed on the associated
+                        // SipDelegate.
+                        mSipDelegate.sendMessage(sipMessage, configVersion);
+                        logi("sendMessage: message sent - " + sipMessage + ", configVersion: "
+                                + configVersion);
+                    } catch (RemoteException e) {
+                        notifyDelegateSendError("RemoteException: " + e, sipMessage,
+                                SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_DEAD);
+                    }
+                });
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        /**
+         * The SipDelegateConnection is requesting that the resources associated with an ongoing SIP
+         * dialog be released as the SIP dialog is now closed.
+         */
+        @Override
+        public void closeDialog(String callId) {
+            long token = Binder.clearCallingIdentity();
+            try {
+                mExecutor.execute(() -> {
+                    if (mSipDelegate == null) {
+                        logw("closeDialog called when SipDelegate is not associated, callId: "
+                                + callId);
+                        return;
+                    }
+                    try {
+                        // TODO track the SIP Dialogs created/destroyed on the associated
+                        // SipDelegate.
+                        mSipDelegate.closeDialog(callId);
+                    } catch (RemoteException e) {
+                        logw("SipDelegate not available when closeDialog was called "
+                                + "for call id: " + callId);
+                    }
+                });
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+    };
+
+    // SipDelegate(ImsService) -> SipDelegateConnection(IMS Application)
+    private final ISipDelegateMessageCallback.Stub mDelegateConnectionMessageCallback =
+            new ISipDelegateMessageCallback.Stub() {
+        /**
+         * An Incoming SIP Message has been received by the SipDelegate and is being routed
+         * to the IMS application for processing.
+         * <p>
+         * IMS application will call {@link ISipDelegate#notifyMessageReceived(String)} to
+         * acknowledge receipt of this incoming message.
+         */
+        @Override
+        public void onMessageReceived(SipMessage message) {
+            long token = Binder.clearCallingIdentity();
+            try {
+                mExecutor.execute(() -> {
+                    VerificationResult result = verifyIncomingMessage(message);
+                    if (!result.isVerified) {
+                        notifyAppReceiveError("Incoming messages restricted", message,
+                                result.restrictedReason);
+                        return;
+                    }
+                    try {
+                        // TODO track the SIP Dialogs created/destroyed on the associated
+                        //  SipDelegate.
+                        mAppCallback.onMessageReceived(message);
+                        logi("onMessageReceived: received " + message);
+                    } catch (RemoteException e) {
+                        notifyAppReceiveError("RemoteException: " + e, message,
+                                SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_DEAD);
+                    }
+                });
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        /**
+         * An outgoing SIP message sent previously by the SipDelegateConnection to the SipDelegate
+         * using {@link ISipDelegate#sendMessage(SipMessage, int)} as been successfully sent.
+         */
+        @Override
+        public void onMessageSent(String viaTransactionId) {
+            long token = Binder.clearCallingIdentity();
+            try {
+                mExecutor.execute(() -> {
+                    if (mSipDelegate == null) {
+                        logw("Unexpected state, onMessageSent called when SipDelegate is not "
+                                + "associated");
+                    }
+                    try {
+                        mAppCallback.onMessageSent(viaTransactionId);
+                    } catch (RemoteException e) {
+                        logw("Error sending onMessageSent to SipDelegateConnection, remote not"
+                                + "available for transaction ID: " + viaTransactionId);
+                    }
+                });
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        /**
+         * An outgoing SIP message sent previously by the SipDelegateConnection to the SipDelegate
+         * using {@link ISipDelegate#sendMessage(SipMessage, int)} failed to be sent.
+         */
+        @Override
+        public void onMessageSendFailure(String viaTransactionId, int reason) {
+            long token = Binder.clearCallingIdentity();
+            try {
+                mExecutor.execute(() -> {
+                    if (mSipDelegate == null) {
+                        logw("Unexpected state, onMessageSendFailure called when SipDelegate is not"
+                                + "associated");
+                    }
+                    try {
+                        mAppCallback.onMessageSendFailure(viaTransactionId, reason);
+                    } catch (RemoteException e) {
+                        logw("Error sending onMessageSendFailure to SipDelegateConnection, remote"
+                                + " not available for transaction ID: " + viaTransactionId);
+                    }
+                });
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+    };
+
+    private final ISipDelegateMessageCallback mAppCallback;
+    private final Executor mExecutor;
+    private final int mSubId;
+    private final LocalLog mLocalLog = new LocalLog(SipTransportController.LOG_SIZE);
+
+    private ISipDelegate mSipDelegate;
+    private Consumer<Boolean> mPendingClosedConsumer;
+    private int mDelegateClosingReason = -1;
+    private int mDelegateClosedReason = -1;
+
+    public MessageTransportStateTracker(int subId, Executor executor,
+            ISipDelegateMessageCallback appMessageCallback) {
+        mSubId = subId;
+        mAppCallback = appMessageCallback;
+        mExecutor = executor;
+    }
+
+    @Override
+    public void onRegistrationStateChanged(DelegateRegistrationState registrationState) {
+        // TODO: integrate registration changes to SipMessage verification checks.
+    }
+
+    @Override
+    public void onImsConfigurationChanged(SipDelegateImsConfiguration config) {
+        // Not needed for this Tracker
+    }
+
+    /**
+     * Open the transport and allow SIP messages to be sent/received on the delegate specified.
+     * @param delegate The delegate connection to send SIP messages to on the ImsService.
+     * @param deniedFeatureTags Feature tags that have been denied. Outgoing SIP messages relating
+     *         to these tags will be denied.
+     */
+    public void openTransport(ISipDelegate delegate, Set<FeatureTagState> deniedFeatureTags) {
+        mSipDelegate = delegate;
+        mDelegateClosingReason = -1;
+        mDelegateClosedReason = -1;
+        // TODO: integrate denied tags to SipMessage verification checks.
+    }
+
+    /** Dump state about this tracker that should be included in the dumpsys */
+    public void dump(PrintWriter printWriter) {
+        mLocalLog.dump(printWriter);
+    }
+
+    /**
+     * @return SipDelegate implementation to be sent to IMS application.
+     */
+    public ISipDelegate getDelegateConnection() {
+        return mSipDelegateConnection;
+    }
+
+    /**
+     * @return MessageCallback implementation to be sent to the ImsService.
+     */
+    public ISipDelegateMessageCallback getMessageCallback() {
+        return mDelegateConnectionMessageCallback;
+    }
+
+    /**
+     * Gradually close all SIP Dialogs by:
+     * 1) denying all new outgoing SIP Dialog requests with the reason specified and
+     * 2) only allowing existing SIP Dialogs to continue.
+     * <p>
+     * This will allow traffic to continue on existing SIP Dialogs until a BYE is sent and the
+     * SIP Dialogs are closed or a timeout is hit and {@link SipDelegate#closeDialog(String)} is
+     * forcefully called on all open SIP Dialogs.
+     * <p>
+     * Any outgoing out-of-dialog traffic on this transport will be denied with the provided reason.
+     * <p>
+     * Incoming out-of-dialog traffic will continue to be set up until the SipDelegate is fully
+     * closed.
+     * @param delegateClosingReason The reason code to return to
+     * {@link DelegateMessageCallback#onMessageSendFailure(String, int)} if a new out-of-dialog SIP
+     *         message is received while waiting for existing Dialogs.
+     * @param closedReason reason to return to new outgoing SIP messages via
+     *         {@link SipDelegate#notifyMessageReceiveError(String, int)} once the transport
+     *         transitions to the fully closed state.
+     * @param resultConsumer The consumer called when the message transport has been closed. It will
+     *         return {@code true} if the procedure completed successfully or {@link false} if the
+     *         transport needed to be closed forcefully due to the application not responding before
+     *         a timeout occurred.
+     */
+    public void closeGracefully(int delegateClosingReason, int closedReason,
+            Consumer<Boolean> resultConsumer) {
+        mDelegateClosingReason = delegateClosingReason;
+        mPendingClosedConsumer = resultConsumer;
+        mExecutor.execute(() -> {
+            // TODO: Track SIP Dialogs and complete when there are no SIP dialogs open anymore or
+            //  the timeout occurs.
+            mPendingClosedConsumer.accept(true);
+            mPendingClosedConsumer = null;
+            closeTransport(closedReason);
+        });
+    }
+
+    /**
+     * Close all ongoing SIP Dialogs immediately and respond to any incoming/outgoing messages with
+     * the provided reason.
+     * @param closedReason The failure reason to provide to incoming/outgoing SIP messages
+     *         if an attempt is made to send/receive a message after this method is called.
+     */
+    public void close(int closedReason) {
+        closeTransport(closedReason);
+    }
+
+    // Clean up all state related to the existing SipDelegate immediately.
+    private void closeTransport(int closedReason) {
+        // TODO: add logic to forcefully close open SIP dialogs once they are being tracked.
+        mSipDelegate = null;
+        if (mPendingClosedConsumer != null) {
+            mExecutor.execute(() -> {
+                logw("closeTransport: transport close forced with pending consumer.");
+                mPendingClosedConsumer.accept(false /*closedGracefully*/);
+                mPendingClosedConsumer = null;
+            });
+        }
+        mDelegateClosingReason = -1;
+        mDelegateClosedReason = closedReason;
+    }
+
+    private VerificationResult verifyOutgoingMessage(SipMessage message) {
+        if (mDelegateClosingReason > -1) {
+            return new VerificationResult(mDelegateClosingReason);
+        }
+        if (mDelegateClosedReason > -1) {
+            return new VerificationResult(mDelegateClosedReason);
+        }
+        if (mSipDelegate == null) {
+            logw("sendMessage called when SipDelegate is not associated." + message);
+            return new VerificationResult(SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_DEAD);
+        }
+        return VerificationResult.SUCCESS;
+    }
+
+    private VerificationResult verifyIncomingMessage(SipMessage message) {
+        // Do not restrict incoming based on closing reason.
+        if (mDelegateClosedReason > -1) {
+            return new VerificationResult(mDelegateClosedReason);
+        }
+        return VerificationResult.SUCCESS;
+    }
+
+    private void notifyDelegateSendError(String logReason, SipMessage message, int reasonCode) {
+        // TODO parse SipMessage header for viaTransactionId.
+        logw("Error sending SipMessage[id: " + null + ", code: " + reasonCode + "] -> SipDelegate "
+                + "for reason: " + logReason);
+        try {
+            mAppCallback.onMessageSendFailure(null, reasonCode);
+        } catch (RemoteException e) {
+            logw("notifyDelegateSendError, SipDelegate is not available: " + e);
+        }
+    }
+
+    private void notifyAppReceiveError(String logReason, SipMessage message, int reasonCode) {
+        // TODO parse SipMessage header for viaTransactionId.
+        logw("Error sending SipMessage[id: " + null + ", code: " + reasonCode + "] -> "
+                + "SipDelegateConnection for reason: " + logReason);
+        try {
+            mSipDelegate.notifyMessageReceiveError(null, reasonCode);
+        } catch (RemoteException e) {
+            logw("notifyAppReceiveError, SipDelegate is not available: " + e);
+        }
+    }
+
+    private void logi(String log) {
+        Log.w(SipTransportController.LOG_TAG, TAG + "[" + mSubId + "] " + log);
+        mLocalLog.log("[I] " + log);
+    }
+
+    private void logw(String log) {
+        Log.w(SipTransportController.LOG_TAG, TAG + "[" + mSubId + "] " + log);
+        mLocalLog.log("[W] " + log);
+    }
+}
diff --git a/src/com/android/services/telephony/rcs/RcsFeatureController.java b/src/com/android/services/telephony/rcs/RcsFeatureController.java
index 8c6fce0..304a74d 100644
--- a/src/com/android/services/telephony/rcs/RcsFeatureController.java
+++ b/src/com/android/services/telephony/rcs/RcsFeatureController.java
@@ -74,6 +74,12 @@
          * Called when the feature should be destroyed.
          */
         void onDestroy();
+
+        /**
+         * Called when a dumpsys is being generated for this RcsFeatureController for all Features
+         * to report their status.
+         */
+        void dump(PrintWriter pw);
     }
 
     /**
@@ -427,6 +433,14 @@
         pw.print("connected=");
         synchronized (mLock) {
             pw.println(mFeatureManager != null);
+            pw.println();
+            pw.println("RcsFeatureControllers:");
+            pw.increaseIndent();
+            for (Feature f : mFeatures.values()) {
+                f.dump(pw);
+                pw.println();
+            }
+            pw.decreaseIndent();
         }
     }
 
diff --git a/src/com/android/services/telephony/rcs/SipDelegateBinderConnection.java b/src/com/android/services/telephony/rcs/SipDelegateBinderConnection.java
new file mode 100644
index 0000000..1a77f2b
--- /dev/null
+++ b/src/com/android/services/telephony/rcs/SipDelegateBinderConnection.java
@@ -0,0 +1,236 @@
+/*
+ * 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.os.Binder;
+import android.os.IBinder;
+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.telephony.ims.stub.SipDelegate;
+import android.util.LocalLog;
+import android.util.Log;
+
+import java.io.PrintWriter;
+import java.util.Collections;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+/**
+ * Container for the active connection to the {@link SipDelegate} active on the ImsService.
+ * <p>
+ * New instances of this class will be created and destroyed new {@link SipDelegate}s are created
+ * and destroyed by the {@link SipDelegateController}.
+ */
+public class SipDelegateBinderConnection implements DelegateBinderStateManager,
+        IBinder.DeathRecipient {
+    private static final String LOG_TAG = "BinderConn";
+
+    protected final int mSubId;
+    protected final Set<FeatureTagState> mDeniedTags;
+    protected final Executor mExecutor;
+    protected final List<StateCallback> mStateCallbacks;
+
+    private final LocalLog mLocalLog = new LocalLog(SipTransportController.LOG_SIZE);
+
+    // Callback interface from ImsService to this Connection. State Events will be forwarded to IMS
+    // application through DelegateStateTracker.
+    private final ISipDelegateStateCallback mSipDelegateStateCallback =
+            new ISipDelegateStateCallback.Stub() {
+                @Override
+                public void onCreated(ISipDelegate delegate,
+                        List<FeatureTagState> deniedFeatureTags) {
+                    long token = Binder.clearCallingIdentity();
+                    try {
+                        mExecutor.execute(() ->
+                                notifySipDelegateCreated(delegate, deniedFeatureTags));
+                    } finally {
+                        Binder.restoreCallingIdentity(token);
+                    }
+                }
+
+                @Override
+                public void onFeatureTagRegistrationChanged(
+                        DelegateRegistrationState registrationState) {
+                    long token = Binder.clearCallingIdentity();
+                    try {
+                        mExecutor.execute(() -> {
+                            logi("onFeatureTagRegistrationChanged:" + registrationState);
+                            for (StateCallback c : mStateCallbacks) {
+                                c.onRegistrationStateChanged(registrationState);
+                            }
+                        });
+                    } finally {
+                        Binder.restoreCallingIdentity(token);
+                    }
+                }
+
+                @Override
+                public void onImsConfigurationChanged(
+                        SipDelegateImsConfiguration registeredSipConfig) {
+                    long token = Binder.clearCallingIdentity();
+                    try {
+                        mExecutor.execute(() -> {
+                            logi("onImsConfigurationChanged");
+                            for (StateCallback c : mStateCallbacks) {
+                                c.onImsConfigurationChanged(registeredSipConfig);
+                            }
+                        });
+                    } finally {
+                        Binder.restoreCallingIdentity(token);
+                    }
+                }
+
+                @Override
+                public void onDestroyed(int reason) {
+                    long token = Binder.clearCallingIdentity();
+                    try {
+                        mExecutor.execute(() -> notifySipDelegateDestroyed(reason));
+                    } finally {
+                        Binder.restoreCallingIdentity(token);
+                    }
+                }
+            };
+
+    private final ISipTransport mSipTransport;
+    private final DelegateRequest mRequestedConfig;
+
+    private ISipDelegate mDelegateBinder;
+    private BiConsumer<ISipDelegate, Set<FeatureTagState>> mPendingCreatedConsumer;
+    private Consumer<Integer> mPendingDestroyedConsumer;
+
+    /**
+     * Create a new Connection object to manage the creation and destruction of a
+     * {@link SipDelegate}.
+     * @param subId The subid that this SipDelegate is being created for.
+     * @param sipTransport The SipTransport implementation that will be used to manage SipDelegates.
+     * @param requestedConfig The DelegateRequest to be sent to the ImsService.
+     * @param transportDeniedTags The feature tags that have already been denied by the
+     *                            SipTransportController and should not be requested.
+     * @param executor The Executor that all binder calls from the remote process will be executed
+     *                on.
+     * @param stateCallbacks A list of callbacks that will each be called when the state of the
+     *                       SipDelegate changes. This will be called on the supplied executor.
+     */
+    public SipDelegateBinderConnection(int subId, ISipTransport sipTransport,
+            DelegateRequest requestedConfig, Set<FeatureTagState> transportDeniedTags,
+            Executor executor, List<StateCallback> stateCallbacks) {
+        mSubId = subId;
+        mSipTransport = sipTransport;
+        mRequestedConfig = requestedConfig;
+        mDeniedTags = transportDeniedTags;
+        mExecutor = executor;
+        mStateCallbacks = stateCallbacks;
+    }
+
+    @Override
+    public boolean create(ISipDelegateMessageCallback cb,
+            BiConsumer<ISipDelegate, Set<FeatureTagState>> createdConsumer) {
+        try {
+            mSipTransport.createSipDelegate(mSubId, mRequestedConfig, mSipDelegateStateCallback,
+                    cb);
+            mSipTransport.asBinder().linkToDeath(this, 0);
+        } catch (RemoteException e) {
+            logw("create called on unreachable SipTransport:" + e);
+            return false;
+        }
+        mPendingCreatedConsumer = createdConsumer;
+        return true;
+    }
+
+    @Override
+    public void destroy(int reason, Consumer<Integer> destroyedConsumer) {
+        mPendingDestroyedConsumer = destroyedConsumer;
+        try {
+            if (mDelegateBinder != null) {
+                mSipTransport.destroySipDelegate(mDelegateBinder, reason);
+            } else {
+                mExecutor.execute(() -> notifySipDelegateDestroyed(reason));
+            }
+            mStateCallbacks.clear();
+        } catch (RemoteException e) {
+            logw("destroy called on unreachable SipTransport:" + e);
+            mExecutor.execute(() -> notifySipDelegateDestroyed(reason));
+        }
+        try {
+            mSipTransport.asBinder().unlinkToDeath(this, 0);
+        } catch (NoSuchElementException e) {
+            logw("unlinkToDeath called on already unlinked binder" + e);
+        }
+    }
+
+    private void notifySipDelegateCreated(ISipDelegate delegate,
+            List<FeatureTagState> deniedFeatureTags) {
+        logi("Delegate Created: " + delegate + ", deniedTags:" + deniedFeatureTags);
+        if (delegate == null) {
+            logw("Invalid null delegate returned!");
+        }
+        mDelegateBinder = delegate;
+        // Add denied feature tags from SipDelegate to the ones denied by the transport
+        if (deniedFeatureTags != null) {
+            mDeniedTags.addAll(deniedFeatureTags);
+        }
+        if (mPendingCreatedConsumer == null) return;
+        mPendingCreatedConsumer.accept(delegate, mDeniedTags);
+        mPendingCreatedConsumer = null;
+    }
+
+    private void notifySipDelegateDestroyed(int reason) {
+        logi("Delegate Destroyed, reason: " + reason);
+        mDelegateBinder = null;
+        if (mPendingDestroyedConsumer == null) return;
+        mPendingDestroyedConsumer.accept(reason);
+        mPendingDestroyedConsumer = null;
+    }
+
+    /** Dump state about this binder connection that should be included in the dumpsys. */
+    public void dump(PrintWriter printWriter) {
+        mLocalLog.dump(printWriter);
+    }
+
+    protected final void logi(String log) {
+        Log.i(SipTransportController.LOG_TAG, LOG_TAG + "[" + mSubId + "] " + log);
+        mLocalLog.log("[I] " + log);
+    }
+
+    protected final void logw(String log) {
+        Log.w(SipTransportController.LOG_TAG, LOG_TAG + "[" + mSubId + "] " + log);
+        mLocalLog.log("[W] " + log);
+    }
+
+    @Override
+    public void binderDied() {
+        mExecutor.execute(() -> {
+            logw("binderDied!");
+            // Unblock any pending create/destroy operations.
+            // SipTransportController will handle the overall destruction/teardown.
+            notifySipDelegateCreated(null, Collections.emptyList());
+            notifySipDelegateDestroyed(SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SERVICE_DEAD);
+        });
+    }
+}
diff --git a/src/com/android/services/telephony/rcs/SipDelegateBinderConnectionStub.java b/src/com/android/services/telephony/rcs/SipDelegateBinderConnectionStub.java
new file mode 100644
index 0000000..888af94
--- /dev/null
+++ b/src/com/android/services/telephony/rcs/SipDelegateBinderConnectionStub.java
@@ -0,0 +1,80 @@
+/*
+ * 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.telephony.ims.DelegateRegistrationState;
+import android.telephony.ims.FeatureTagState;
+import android.telephony.ims.aidl.ISipDelegate;
+import android.telephony.ims.aidl.ISipDelegateMessageCallback;
+import android.telephony.ims.stub.SipDelegate;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+/**
+ * Stub implementation used when a SipDelegate needs to be set up in specific cases, but there
+ * is no underlying implementation in the ImsService.
+ *
+ * This is used in cases where all of the requested feature tags were denied for various reasons
+ * from the SipTransportController. In this case, we will "connect", send a update to include the
+ * denied feature tags, and then do nothing until this stub is torn down.
+ */
+public class SipDelegateBinderConnectionStub implements DelegateBinderStateManager {
+    protected final Set<FeatureTagState> mDeniedTags;
+    protected final Executor mExecutor;
+    protected final List<StateCallback> mStateCallbacks;
+
+    /**
+     * Create a new Connection object to manage the creation and destruction of a
+     * {@link SipDelegate}.
+     * @param transportDeniedTags The feature tags that have already been denied by the
+     *                            SipTransportController and should not be requested.
+     * @param executor The Executor that all binder calls from the remote process will be executed
+     *                on.
+     * @param stateCallbacks A list of callbacks that will each be called when the state of the
+     *                       SipDelegate changes. This will be called on the supplied executor.
+     */
+    public SipDelegateBinderConnectionStub(Set<FeatureTagState> transportDeniedTags,
+            Executor executor, List<StateCallback> stateCallbacks) {
+        mDeniedTags = transportDeniedTags;
+        mExecutor = executor;
+        mStateCallbacks = stateCallbacks;
+    }
+
+    @Override
+    public boolean create(ISipDelegateMessageCallback cb,
+            BiConsumer<ISipDelegate, Set<FeatureTagState>> createdConsumer) {
+        mExecutor.execute(() -> {
+            createdConsumer.accept(null, (mDeniedTags));
+            for (SipDelegateBinderConnection.StateCallback  c: mStateCallbacks) {
+                c.onRegistrationStateChanged(new DelegateRegistrationState.Builder().build());
+            }
+        });
+        return true;
+    }
+
+    @Override
+    public void destroy(int reason, Consumer<Integer> destroyedConsumer) {
+        mExecutor.execute(() -> {
+            mStateCallbacks.clear();
+            destroyedConsumer.accept(reason);
+        });
+    }
+}
diff --git a/src/com/android/services/telephony/rcs/SipDelegateController.java b/src/com/android/services/telephony/rcs/SipDelegateController.java
new file mode 100644
index 0000000..ed50778
--- /dev/null
+++ b/src/com/android/services/telephony/rcs/SipDelegateController.java
@@ -0,0 +1,375 @@
+/*
+ * 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.telephony.ims.DelegateRegistrationState;
+import android.telephony.ims.DelegateRequest;
+import android.telephony.ims.FeatureTagState;
+import android.telephony.ims.SipDelegateConnection;
+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.telephony.ims.stub.DelegateConnectionStateCallback;
+import android.telephony.ims.stub.SipDelegate;
+import android.util.LocalLog;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ScheduledExecutorService;
+
+/**
+ * Created when an IMS application wishes to open up a {@link SipDelegateConnection} and manages the
+ * resulting {@link SipDelegate} that may be created on the ImsService side.
+ */
+public class SipDelegateController {
+    static final String LOG_TAG = "SipDelegateC";
+
+    private DelegateBinderStateManager.Factory mBinderConnectionFactory =
+            new DelegateBinderStateManager.Factory() {
+        @Override
+        public DelegateBinderStateManager create(int subId, ISipTransport sipTransport,
+                DelegateRequest requestedConfig, Set<FeatureTagState> transportDeniedTags,
+                Executor executor, List<DelegateBinderStateManager.StateCallback> stateCallbacks) {
+            // We should not actually create a SipDelegate in this case.
+            if (requestedConfig.getFeatureTags().isEmpty()) {
+                return new SipDelegateBinderConnectionStub(transportDeniedTags, executor,
+                        stateCallbacks);
+            }
+            return new SipDelegateBinderConnection(mSubId, mSipTransportImpl, requestedConfig,
+                    transportDeniedTags, mExecutorService, stateCallbacks);
+        }
+    };
+
+    private final int mSubId;
+    private final String mPackageName;
+    private final DelegateRequest mInitialRequest;
+    private final ISipTransport mSipTransportImpl;
+    private final ScheduledExecutorService mExecutorService;
+    private final MessageTransportStateTracker mMessageTransportStateTracker;
+    private final DelegateStateTracker mDelegateStateTracker;
+    private final LocalLog mLocalLog = new LocalLog(SipTransportController.LOG_SIZE);
+
+    private DelegateBinderStateManager mBinderConnection;
+    private Set<String> mTrackedFeatureTags;
+
+    public SipDelegateController(int subId, DelegateRequest initialRequest, String packageName,
+            ISipTransport sipTransportImpl, ScheduledExecutorService executorService,
+            ISipDelegateConnectionStateCallback stateCallback,
+            ISipDelegateMessageCallback messageCallback) {
+        mSubId = subId;
+        mPackageName = packageName;
+        mInitialRequest = initialRequest;
+        mSipTransportImpl = sipTransportImpl;
+        mExecutorService = executorService;
+
+        mMessageTransportStateTracker = new MessageTransportStateTracker(mSubId, executorService,
+                messageCallback);
+        mDelegateStateTracker = new DelegateStateTracker(mSubId, stateCallback,
+                mMessageTransportStateTracker.getDelegateConnection());
+    }
+
+    /**
+     * Inject dependencies for testing only.
+     */
+    @VisibleForTesting
+    public SipDelegateController(int subId, DelegateRequest initialRequest, String packageName,
+            ISipTransport sipTransportImpl, ScheduledExecutorService executorService,
+            MessageTransportStateTracker messageTransportStateTracker,
+            DelegateStateTracker delegateStateTracker,
+            DelegateBinderStateManager.Factory connectionFactory) {
+        mSubId = subId;
+        mInitialRequest = initialRequest;
+        mPackageName = packageName;
+        mSipTransportImpl = sipTransportImpl;
+        mExecutorService = executorService;
+        mMessageTransportStateTracker = messageTransportStateTracker;
+        mDelegateStateTracker = delegateStateTracker;
+        mBinderConnectionFactory = connectionFactory;
+    }
+
+    /**
+     * @return The InitialRequest from the IMS application. The feature tags that are actually set
+     * up may differ from this request based on the state of this controller.
+     */
+    public DelegateRequest getInitialRequest() {
+        return mInitialRequest;
+    }
+
+    /**
+     * @return The package name of the IMS application associated with this SipDelegateController.
+     */
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    public ISipDelegate getSipDelegateInterface() {
+        return mMessageTransportStateTracker.getDelegateConnection();
+    }
+
+    /**
+     * Create the underlying SipDelegate.
+     * <p>
+     * This may not happen instantly, The CompletableFuture returned will not complete until
+     * {@link DelegateConnectionStateCallback#onCreated(SipDelegateConnection)} is called by the
+     * SipDelegate or DelegateStateTracker state is updated in the case that all requested features
+     * were denied.
+     * @return A CompletableFuture that will complete once the SipDelegate has been created. If true
+     * is returned, the SipDelegate has been created successfully. If false, the ImsService is not
+     * reachable and the process should be aborted.
+     */
+    public CompletableFuture<Boolean> create(Set<String> supportedSet,
+            Set<FeatureTagState> deniedSet) {
+        logi("create, supported: " + supportedSet + ", denied: " + deniedSet);
+        mTrackedFeatureTags = supportedSet;
+        DelegateBinderStateManager connection =
+                createBinderConnection(supportedSet, deniedSet);
+        CompletableFuture<Pair<ISipDelegate, Set<FeatureTagState>>> pendingCreate =
+                createSipDelegate(connection);
+        // May need to implement special case handling where SipDelegate denies all in supportedSet,
+        // however that should be a very rare case. For now, if that happens, just keep the
+        // SipDelegate bound.
+        // use thenApply here because we need this to happen on the same thread that it was called
+        // on in order to ensure ordering of onCreated being called, followed by registration
+        // state changed. If not, this is subject to race conditions where registered is queued
+        // before the async processing of this future.
+        return pendingCreate.thenApply((resultPair) -> {
+            if (resultPair == null) {
+                logw("create: resultPair returned null");
+                return false;
+            }
+            mBinderConnection = connection;
+            logi("create: created, delegate denied: " + resultPair.second);
+            mMessageTransportStateTracker.openTransport(resultPair.first, resultPair.second);
+            mDelegateStateTracker.sipDelegateConnected(resultPair.second);
+            return true;
+        });
+    }
+
+    /**
+     * Modify the SipTransport to reflect the new Feature Tag set that the IMS application has
+     * access to.
+     * <p>
+     * This involves the following operations if the new supported tag set does not match the
+     * the existing set:
+     * 1) destroy the existing underlying SipDelegate. If there are SIP Dialogs that are active
+     * on the SipDelegate that is pending to be destroyed, we must move the feature tags into a
+     * deregistering state via
+     * {@link DelegateRegistrationState#DEREGISTERING_REASON_FEATURE_TAGS_CHANGING} to signal to the
+     * IMS application to close all dialogs before the operation can proceed. If any outgoing
+     * out-of-dialog messages are sent at this time, they will also fail with reason
+     * {@link SipDelegateManager#MESSAGE_FAILURE_REASON_INTERNAL_DELEGATE_STATE_TRANSITION}.
+     * 2) create a new underlying SipDelegate and notify trackers, allowing the transport to
+     * re-open.
+     * @param newSupportedSet The new supported set of feature tags that the SipDelegate should
+     *     be opened for.
+     * @param deniedSet The new set of tags that have been denied as well as the reason for the
+     *        denial to be reported back to the IMS Application.
+     * @return A CompletableFuture containing the pending operation that will change the supported
+     * feature tags. Any operations to change the supported feature tags of the associated
+     * SipDelegate after this should not happen until this pending operation completes. Will
+     * complete with {@code true} if the operation was successful or {@code false} if the
+     * IMS service was unreachable.
+     */
+    public CompletableFuture<Boolean> changeSupportedFeatureTags(Set<String> newSupportedSet,
+            Set<FeatureTagState> deniedSet) {
+        logi("Received feature tag set change, old: [" + mTrackedFeatureTags + "], new: "
+                + newSupportedSet + ",denied: [" + deniedSet + "]");
+        if (mTrackedFeatureTags != null && mTrackedFeatureTags.equals(newSupportedSet)) {
+            logi("changeSupportedFeatureTags: no change, returning");
+            return CompletableFuture.completedFuture(true);
+        }
+
+        mTrackedFeatureTags = newSupportedSet;
+        // Next perform the destroy operation.
+        CompletableFuture<Integer> pendingDestroy = destroySipDelegate(false/*force*/,
+                SipDelegateManager.MESSAGE_FAILURE_REASON_INTERNAL_DELEGATE_STATE_TRANSITION,
+                SipDelegateManager.MESSAGE_FAILURE_REASON_INTERNAL_DELEGATE_STATE_TRANSITION,
+                DelegateRegistrationState.DEREGISTERING_REASON_FEATURE_TAGS_CHANGING,
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+
+        // Next perform the create operation with the new set of supported feature tags.
+        return pendingDestroy.thenComposeAsync((reasonFromService) -> {
+            logi("changeSupportedFeatureTags: destroy stage complete, reason reported: "
+                    + reasonFromService);
+            return create(newSupportedSet, deniedSet);
+        }, mExecutorService);
+    }
+
+    /**
+     * Destroy this SipDelegate. This controller should be disposed of after this method is
+     * called.
+     * <p>
+     * This may not happen instantly if there are SIP Dialogs that are active on this SipDelegate.
+     * In this case, the CompletableFuture will not complete until
+     * {@link DelegateConnectionStateCallback#onDestroyed(int)} is called by the SipDelegate.
+     * @param force If set true, we will close the transport immediately and call
+     * {@link SipDelegate#closeDialog(String)} on any open dialogs. If false, we will wait for the
+     *         SIP Dialogs to close or the close timer to timeout before destroying the underlying
+     *         SipDelegate.
+     * @param destroyReason The reason for why this SipDelegate is being destroyed.
+     * @return A CompletableFuture that will complete once the SipDelegate has been destroyed.
+     */
+    public CompletableFuture<Integer> destroy(boolean force, int destroyReason) {
+        logi("destroy, forced " + force + ", destroyReason: " + destroyReason);
+
+        CompletableFuture<Integer> pendingOperationComplete =
+                destroySipDelegate(force, SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED,
+                        getMessageFailReasonFromDestroyReason(destroyReason),
+                        DelegateRegistrationState.DEREGISTERING_REASON_DESTROY_PENDING,
+                        destroyReason);
+        return pendingOperationComplete.thenApplyAsync((reasonFromDelegate) -> {
+            logi("destroy, operation complete, notifying trackers, reason" + reasonFromDelegate);
+            mDelegateStateTracker.sipDelegateDestroyed(reasonFromDelegate);
+            return reasonFromDelegate;
+        }, mExecutorService);
+    };
+
+    private static int getMessageFailReasonFromDestroyReason(int destroyReason) {
+        switch (destroyReason) {
+            case SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SERVICE_DEAD:
+                return SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_DEAD;
+            case SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP:
+            case SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_USER_DISABLED_RCS:
+                return SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED;
+            default:
+                return SipDelegateManager.MESSAGE_FAILURE_REASON_UNKNOWN;
+        }
+    }
+
+    /**
+     * @param force If set true, we will close the transport immediately and call
+     * {@link SipDelegate#closeDialog(String)} on any open dialogs. If false, we will wait for the
+     *         SIP Dialogs to close or the close timer to timeout before destroying the underlying
+     *         SipDelegate.
+     * @param messageDestroyingReason The reason to send back to the IMS application in the case
+     *         that a new outgoing SIP message is sent that is out-of-dialog while the message
+     *         transport is closing.
+     * @param messageDestroyedReason The reason to send back to the IMS application in the case
+     *         that a new outgoing SIP message is sent once the underlying transport is closed.
+     * @param deregisteringReason The deregistering state reported to the IMS application for all
+     *         registered feature tags.
+     * @param delegateDestroyedReason The reason to send to the underlying SipDelegate that is being
+     *         destroyed.
+     * @return A CompletableFuture containing the reason from the SipDelegate for why it was
+     * destroyed.
+     */
+    private CompletableFuture<Integer> destroySipDelegate(boolean force,
+            int messageDestroyingReason, int messageDestroyedReason, int deregisteringReason,
+            int delegateDestroyedReason) {
+        if (mBinderConnection == null) {
+            logi("destroySipDelegate, called when binder connection is already null");
+            return CompletableFuture.completedFuture(delegateDestroyedReason);
+        }
+        // First, bring down the message transport.
+        CompletableFuture<Boolean> pendingTransportClosed = new CompletableFuture<>();
+        if (force) {
+            logi("destroySipDelegate, forced");
+            mMessageTransportStateTracker.close(messageDestroyedReason);
+            pendingTransportClosed.complete(true);
+        } else {
+            mMessageTransportStateTracker.closeGracefully(messageDestroyingReason,
+                    messageDestroyedReason, pendingTransportClosed::complete);
+        }
+
+        // Do not send an intermediate pending state to app if there are no open SIP dialogs to
+        // worry about.
+        if (!pendingTransportClosed.isDone()) {
+            mDelegateStateTracker.sipDelegateChanging(deregisteringReason);
+        } else {
+            logi("destroySipDelegate, skip DEREGISTERING_REASON_DESTROY_PENDING");
+        }
+
+        // Next, destroy the SipDelegate.
+        return pendingTransportClosed.thenComposeAsync((wasGraceful) -> {
+            logi("destroySipDelegate, transport gracefully closed = " + wasGraceful);
+            CompletableFuture<Integer> pendingDestroy = new CompletableFuture<>();
+            mBinderConnection.destroy(delegateDestroyedReason, pendingDestroy::complete);
+            return pendingDestroy;
+        }, mExecutorService);
+    }
+
+    /**
+     * @return a CompletableFuture that returns a Pair containing SipDelegate Binder interface as
+     * well as rejected feature tags or a {@code null} Pair instance if the ImsService is not
+     * available.
+     */
+    private CompletableFuture<Pair<ISipDelegate, Set<FeatureTagState>>> createSipDelegate(
+            DelegateBinderStateManager connection) {
+        CompletableFuture<Pair<ISipDelegate, Set<FeatureTagState>>> createdFuture =
+                new CompletableFuture<>();
+        boolean isStarted = connection.create(mMessageTransportStateTracker.getMessageCallback(),
+                (delegate, delegateDeniedTags) ->
+                        createdFuture.complete(new Pair<>(delegate, delegateDeniedTags)));
+        if (!isStarted) {
+            logw("Couldn't create binder connection, ImsService is not available.");
+            connection.destroy(SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SERVICE_DEAD, null);
+            return CompletableFuture.completedFuture(null);
+        }
+        return createdFuture;
+    }
+
+    private DelegateBinderStateManager createBinderConnection(Set<String> supportedSet,
+            Set<FeatureTagState> deniedSet) {
+
+        List<DelegateBinderStateManager.StateCallback> stateCallbacks = new ArrayList<>(2);
+        stateCallbacks.add(mDelegateStateTracker);
+        stateCallbacks.add(mMessageTransportStateTracker);
+
+        return mBinderConnectionFactory.create(mSubId, mSipTransportImpl,
+                new DelegateRequest(supportedSet), deniedSet, mExecutorService, stateCallbacks);
+    }
+
+    /**
+     * Write the current state of this controller in String format using the PrintWriter provided
+     * for dumpsys.
+     */
+    public void dump(PrintWriter printWriter) {
+        IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, "  ");
+        pw.println("SipDelegateController" + "[" + mSubId + "]:");
+        pw.increaseIndent();
+        pw.println("DelegateStateTracker:");
+        pw.increaseIndent();
+        mDelegateStateTracker.dump(printWriter);
+        pw.decreaseIndent();
+        pw.println("MessageStateTracker:");
+        pw.increaseIndent();
+        mMessageTransportStateTracker.dump(printWriter);
+        pw.decreaseIndent();
+        pw.decreaseIndent();
+    }
+
+    private void logi(String log) {
+        Log.w(SipTransportController.LOG_TAG, LOG_TAG + "[" + mSubId + "] " + log);
+        mLocalLog.log("[I] " + log);
+    }
+
+    private void logw(String log) {
+        Log.w(SipTransportController.LOG_TAG, LOG_TAG + "[" + mSubId + "] " + log);
+        mLocalLog.log("[W] " + log);
+    }
+}
diff --git a/src/com/android/services/telephony/rcs/SipTransportController.java b/src/com/android/services/telephony/rcs/SipTransportController.java
index da5374a..5d817ba 100644
--- a/src/com/android/services/telephony/rcs/SipTransportController.java
+++ b/src/com/android/services/telephony/rcs/SipTransportController.java
@@ -16,19 +16,51 @@
 
 package com.android.services.telephony.rcs;
 
+import android.app.role.OnRoleHoldersChangedListener;
+import android.app.role.RoleManager;
 import android.content.Context;
+import android.os.UserHandle;
+import android.telephony.ims.DelegateRequest;
+import android.telephony.ims.FeatureTagState;
 import android.telephony.ims.ImsException;
 import android.telephony.ims.ImsService;
+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.telephony.ims.stub.DelegateConnectionMessageCallback;
+import android.telephony.ims.stub.DelegateConnectionStateCallback;
+import android.telephony.ims.stub.SipDelegate;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.LocalLog;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
+
 import com.android.ims.RcsFeatureManager;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
 
+import com.google.common.base.Objects;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
 import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
 
 /**
  * Manages the creation and destruction of SipDelegates in response to an IMS application requesting
@@ -39,16 +71,179 @@
  * 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.
+ *
+ * //TODO: Support other roles besides SMS
+ * //TODO: Bring in carrier provisioning to influence features that can be created.
+ * //TODO: Generate registration change events.
  */
-public class SipTransportController implements RcsFeatureController.Feature {
-    private static final String LOG_TAG = "SipTransportC";
+public class SipTransportController implements RcsFeatureController.Feature,
+        OnRoleHoldersChangedListener {
+    static final int LOG_SIZE = 50;
+    static final String LOG_TAG = "SipTransportC";
 
-    private final Context mContext;
+    /**See {@link TimerAdapter#getReevaluateThrottleTimerMilliseconds()}.*/
+    private static final int REEVALUATE_THROTTLE_DEFAULT_MS = 1000;
+    /**See {@link TimerAdapter#getUpdateRegistrationDelayMilliseconds()}.*/
+    private static final int TRIGGER_UPDATE_REGISTRATION_DELAY_DEFAULT_MS = 1000;
+
+    /**
+     * {@link RoleManager} is final so we have to wrap the implementation for testing.
+     */
+    @VisibleForTesting
+    public interface RoleManagerAdapter {
+        /** See {@link RoleManager#getRoleHolders(String)} */
+        List<String> getRoleHolders(String roleName);
+        /** See {@link RoleManager#addOnRoleHoldersChangedListenerAsUser} */
+        void addOnRoleHoldersChangedListenerAsUser(Executor executor,
+                OnRoleHoldersChangedListener listener, UserHandle user);
+        /** See {@link RoleManager#removeOnRoleHoldersChangedListenerAsUser} */
+        void removeOnRoleHoldersChangedListenerAsUser(OnRoleHoldersChangedListener listener,
+                UserHandle user);
+    }
+
+    /**
+     * Adapter for timers related to this class so they can be modified during testing.
+     */
+    @VisibleForTesting
+    public interface TimerAdapter {
+        /**
+         * Time we will delay after a {@link #createSipDelegate} or {@link #destroySipDelegate}
+         * command to re-evaluate and apply any changes to the list of tracked
+         * SipDelegateControllers.
+         * <p>
+         * Another create/destroy request sent during this time will not postpone re-evaluation
+         * again.
+         */
+        int getReevaluateThrottleTimerMilliseconds();
+
+        /**
+         * Time after re-evaluate we will wait to trigger the update of IMS registration.
+         * <p>
+         * Another re-evaluate while waiting to trigger a registration update will cause this
+         * controller to cancel and reschedule the event again, further delaying the trigger to send
+         * a registration update.
+         */
+        int getUpdateRegistrationDelayMilliseconds();
+    }
+
+    private static class TimerAdapterImpl implements TimerAdapter {
+
+        @Override
+        public int getReevaluateThrottleTimerMilliseconds() {
+            return REEVALUATE_THROTTLE_DEFAULT_MS;
+        }
+
+        @Override
+        public int getUpdateRegistrationDelayMilliseconds() {
+            return TRIGGER_UPDATE_REGISTRATION_DELAY_DEFAULT_MS;
+        }
+    }
+
+    private static class RoleManagerAdapterImpl implements RoleManagerAdapter {
+
+        private final RoleManager mRoleManager;
+
+        private RoleManagerAdapterImpl(Context context) {
+            mRoleManager = context.getSystemService(RoleManager.class);
+        }
+
+        @Override
+        public List<String> getRoleHolders(String roleName) {
+            return mRoleManager.getRoleHolders(roleName);
+        }
+
+        @Override
+        public void addOnRoleHoldersChangedListenerAsUser(Executor executor,
+                OnRoleHoldersChangedListener listener, UserHandle user) {
+            mRoleManager.addOnRoleHoldersChangedListenerAsUser(executor, listener, user);
+        }
+
+        @Override
+        public void removeOnRoleHoldersChangedListenerAsUser(OnRoleHoldersChangedListener listener,
+                UserHandle user) {
+            mRoleManager.removeOnRoleHoldersChangedListenerAsUser(listener, user);
+        }
+    }
+
+    /**
+     * Used in {@link #destroySipDelegate(int, ISipDelegate, int)} to store pending destroy
+     * requests.
+     */
+    private static final class DestroyRequest {
+        public final SipDelegateController controller;
+        public final int reason;
+
+        DestroyRequest(SipDelegateController c, int r) {
+            controller = c;
+            reason = r;
+        }
+
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            DestroyRequest that = (DestroyRequest) o;
+            return reason == that.reason
+                    && controller.equals(that.controller);
+        }
+
+        @Override
+        public int hashCode() {
+            return java.util.Objects.hash(controller, reason);
+        }
+
+        @Override
+        public String toString() {
+            return "DestroyRequest{" + "controller=" + controller + ", reason=" + reason + '}';
+        }
+    }
+
+    /**
+     * Allow the ability for tests to easily mock out the SipDelegateController for testing.
+     */
+    @VisibleForTesting
+    public interface SipDelegateControllerFactory {
+        /** See {@link SipDelegateController} */
+        SipDelegateController create(int subId, DelegateRequest initialRequest, String packageName,
+                ISipTransport sipTransportImpl, ScheduledExecutorService executorService,
+                ISipDelegateConnectionStateCallback stateCallback,
+                ISipDelegateMessageCallback messageCallback);
+    }
+
+    private SipDelegateControllerFactory mDelegateControllerFactory = SipDelegateController::new;
     private final int mSlotId;
     private final ScheduledExecutorService mExecutorService;
+    private final RoleManagerAdapter mRoleManagerAdapter;
+    private final TimerAdapter mTimerAdapter;
+    private final LocalLog mLocalLog = new LocalLog(LOG_SIZE);
 
+    // A priority queue of active SipDelegateControllers, where the oldest SipDelegate gets
+    // access to the feature tag if multiple apps are allowed to request the same feature tag.
+    private final List<SipDelegateController> mDelegatePriorityQueue = new ArrayList<>();
+    // SipDelegateControllers who have been created and are pending to be added to the priority
+    // queue. Will be added into the queue in the same order as they were added here.
+    private final List<SipDelegateController> mDelegatePendingCreate = new ArrayList<>();
+    // SipDelegateControllers that are pending to be destroyed.
+    private final List<DestroyRequest> mDelegatePendingDestroy = new ArrayList<>();
+
+    // Future scheduled for operations that require the list of SipDelegateControllers to
+    // be evaluated. When the timer expires and triggers the reevaluate method, this controller
+    // will iterate through mDelegatePriorityQueue and assign Feature Tags based on role+priority.
+    private ScheduledFuture<?> mScheduledEvaluateFuture;
+    // mPendingEvaluateFTFuture creates this CompletableFuture, exposed in order to stop other
+    // evaluates from occurring while another is waiting for a result on other threads.
+    private CompletableFuture<Void> mEvaluateCompleteFuture;
+    // Future scheduled that will trigger the ImsService to update the IMS registration for the
+    // SipDelegate configuration. Will be scheduled TRIGGER_UPDATE_REGISTRATION_DELAY_MS
+    // milliseconds after a pending evaluate completes.
+    private ScheduledFuture<?> mPendingUpdateRegistrationFuture;
+    // Subscription id will change as new subscriptions are loaded on the slot.
     private int mSubId;
+    // Will go up/down as the ImsService associated with this slotId goes up/down.
     private RcsFeatureManager mRcsManager;
+    // Cached package name of the app that is considered the default SMS app.
+    private String mCachedSmsRolePackageName = "";
 
     /**
      * Create an instance of SipTransportController.
@@ -57,10 +252,11 @@
      * @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;
 
+        mRoleManagerAdapter = new RoleManagerAdapterImpl(context);
+        mTimerAdapter = new TimerAdapterImpl();
         mExecutorService = Executors.newSingleThreadScheduledExecutor();
     }
 
@@ -69,11 +265,14 @@
      */
     @VisibleForTesting
     public SipTransportController(Context context, int slotId, int subId,
-            ScheduledExecutorService executor) {
-        mContext = context;
+            SipDelegateControllerFactory delegateFactory, RoleManagerAdapter roleManagerAdapter,
+            TimerAdapter timerAdapter, ScheduledExecutorService executor) {
         mSlotId = slotId;
         mSubId = subId;
 
+        mRoleManagerAdapter = roleManagerAdapter;
+        mTimerAdapter = timerAdapter;
+        mDelegateControllerFactory = delegateFactory;
         mExecutorService = executor;
         logi("created");
     }
@@ -95,8 +294,57 @@
 
     @Override
     public void onDestroy() {
-        // Can be null in testing.
-        mExecutorService.shutdownNow();
+        mExecutorService.submit(()-> {
+            // Ensure new create/destroy requests are denied.
+            mSubId = -1;
+            triggerDeregistrationEvent();
+            scheduleDestroyDelegates(
+                    SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SUBSCRIPTION_TORN_DOWN)
+                    .thenRun(mExecutorService::shutdown);
+        });
+    }
+
+    /**
+     * Optionally create a new {@link SipDelegate} based off of the {@link DelegateRequest} given
+     * based on the state of this controller and associate it with the given callbacks.
+     * <p>
+     * Once the {@link SipDelegate} has been created,
+     * {@link ISipDelegateConnectionStateCallback#onCreated(ISipDelegate)} must be called with
+     * the AIDL instance corresponding to the remote {@link SipDelegate}.
+     * @param subId the subId associated with the request.
+     * @param request The request parameters used to create the {@link SipDelegate}.
+     * @param delegateState The {@link DelegateConnectionStateCallback} Binder connection.
+     * @param delegateMessage The {@link DelegateConnectionMessageCallback} Binder Connection
+     * @throws ImsException if the request to create the {@link SipDelegate} did not complete.
+     */
+    public void createSipDelegate(int subId, DelegateRequest request, String packageName,
+            ISipDelegateConnectionStateCallback delegateState,
+            ISipDelegateMessageCallback delegateMessage) throws ImsException {
+        logi("createSipDelegate: request= " + request + ", packageName= " + packageName);
+        CompletableFuture<ImsException> result = new CompletableFuture<>();
+        mExecutorService.submit(() -> createSipDelegateInternal(subId, request, packageName,
+                delegateState,
+                // Capture any ImsExceptions generated during the process.
+                delegateMessage, result::complete));
+        try {
+            ImsException e = result.get();
+            logi("createSipDelegate: request finished");
+            if (e != null) {
+                throw e;
+            }
+        } catch (InterruptedException | ExecutionException e) {
+            logw("createSipDelegate: exception completing future: " + e);
+        }
+    }
+
+    /**
+     * The remote IMS application has requested the destruction of an existing {@link SipDelegate}.
+     * @param subId The subId associated with the request.
+     * @param connection The internal Binder connection associated with the {@link SipDelegate}.
+     * @param reason The reason for why the {@link SipDelegate} was destroyed.
+     */
+    public void destroySipDelegate(int subId, ISipDelegate connection, int reason) {
+        mExecutorService.execute(() -> destroySipDelegateInternal(subId, connection, reason));
     }
 
     /**
@@ -113,6 +361,104 @@
         return result;
     }
 
+    private void createSipDelegateInternal(int subId, DelegateRequest request, String packageName,
+            ISipDelegateConnectionStateCallback delegateState,
+            ISipDelegateMessageCallback delegateMessage,
+            Consumer<ImsException> startedErrorConsumer) {
+        ISipTransport transport;
+        // Send back any errors via Consumer early in creation process if it is clear that the
+        // SipDelegate will never be created.
+        try {
+            checkStateOfController(subId);
+            transport = mRcsManager.getSipTransport();
+            if (transport == null) {
+                logw("createSipDelegateInternal, transport null during request.");
+                startedErrorConsumer.accept(new ImsException("SipTransport not supported",
+                        ImsException.CODE_ERROR_UNSUPPORTED_OPERATION));
+                return;
+            } else {
+                // Release the binder thread as there were no issues processing the initial request.
+                startedErrorConsumer.accept(null);
+            }
+        } catch (ImsException e) {
+            logw("createSipDelegateInternal, ImsException during create: " + e);
+            startedErrorConsumer.accept(e);
+            return;
+        }
+
+        SipDelegateController c = mDelegateControllerFactory.create(subId, request, packageName,
+                transport, mExecutorService, delegateState, delegateMessage);
+        logi("createSipDelegateInternal: request= " + request + ", packageName= " + packageName
+                + ", controller created: " + c);
+        addPendingCreateAndEvaluate(c);
+    }
+
+    private void destroySipDelegateInternal(int subId, ISipDelegate connection, int reason) {
+        if (subId != mSubId) {
+            logw("destroySipDelegateInternal: ignoring destroy, as this is about to be destroyed "
+                    + "anyway due to subId change, delegate=" + connection);
+            return;
+        }
+        if (connection == null) {
+            logw("destroySipDelegateInternal: ignoring destroy, null connection binder.");
+            return;
+        }
+        SipDelegateController match = null;
+        for (SipDelegateController controller : mDelegatePriorityQueue) {
+            if (Objects.equal(connection.asBinder(),
+                    controller.getSipDelegateInterface().asBinder())) {
+                match = controller;
+                break;
+            }
+        }
+        if (match == null) {
+            logw("destroySipDelegateInternal: could not find matching connection=" + connection);
+            return;
+        }
+
+        logi("destroySipDelegateInternal: destroy queued for connection= " + connection);
+        addPendingDestroyAndEvaluate(match, reason);
+    }
+
+    /**
+     * Cancel pending update IMS registration events if they exist and instead send a deregister
+     * event.
+     */
+    private void triggerDeregistrationEvent() {
+        if (mPendingUpdateRegistrationFuture != null
+                && !mPendingUpdateRegistrationFuture.isDone()) {
+            // Cancel pending update and replace with a call to deregister now.
+            mPendingUpdateRegistrationFuture.cancel(false);
+            logi("triggerDeregistrationEvent: cancelling existing reg update event: "
+                    + mPendingUpdateRegistrationFuture);
+        }
+        logi("triggerDeregistrationEvent: Sending deregister event to ImsService");
+        //TODO hook up registration apis
+    }
+
+    /**
+     * Schedule an update to the IMS registration. If there is an existing update scheduled, cancel
+     * it and reschedule.
+     */
+    private void scheduleUpdateRegistration() {
+        if (mPendingUpdateRegistrationFuture != null
+                && !mPendingUpdateRegistrationFuture.isDone()) {
+            // Cancel the old pending operation and reschedule again.
+            mPendingUpdateRegistrationFuture.cancel(false);
+            logi("scheduleUpdateRegistration: cancelling existing reg update event: "
+                    + mPendingUpdateRegistrationFuture);
+        }
+        ScheduledFuture<?> f = mExecutorService.schedule(this::triggerUpdateRegistrationEvent,
+                mTimerAdapter.getUpdateRegistrationDelayMilliseconds(), TimeUnit.MILLISECONDS);
+        logi("scheduleUpdateRegistration: scheduling new event: " + f);
+        mPendingUpdateRegistrationFuture = f;
+    }
+
+    private void triggerUpdateRegistrationEvent() {
+        logi("triggerUpdateRegistrationEvent: Sending update registration event to ImsService");
+        //TODO hook up registration apis
+    }
+
     /**
      * Returns whether or not the ImsService implementation associated with the supplied subId
      * supports the SipTransport APIs.
@@ -126,6 +472,283 @@
         return (mRcsManager.getSipTransport() != null);
     }
 
+    private boolean addPendingDestroy(SipDelegateController c, int reason) {
+        DestroyRequest request = new DestroyRequest(c, reason);
+        if (!mDelegatePendingDestroy.contains(request)) {
+            return mDelegatePendingDestroy.add(request);
+        }
+        return false;
+    }
+
+    /**
+     * The supplied SipDelegateController has been destroyed and associated feature tags have been
+     * released. Trigger the re-evaluation of the priority queue with the new configuration.
+     */
+    private void addPendingDestroyAndEvaluate(SipDelegateController c, int reason) {
+        if (addPendingDestroy(c, reason)) {
+            scheduleThrottledReevaluate();
+        }
+    }
+
+    /**
+     * A new SipDelegateController has been created, add to the back of the priority queue and
+     * trigger the re-evaluation of the priority queue with the new configuration.
+     */
+    private void addPendingCreateAndEvaluate(SipDelegateController c) {
+        mDelegatePendingCreate.add(c);
+        scheduleThrottledReevaluate();
+    }
+
+    /**
+     * The priority queue has changed, which will cause a re-evaluation of the feature tags granted
+     * to each SipDelegate.
+     * <p>
+     * Note: re-evaluations are throttled to happen at a minimum to occur every
+     * REEVALUATE_THROTTLE_MS seconds. We also do not reevaluate while another reevaluate operation
+     * is in progress, so in this case, defer schedule itself.
+     */
+    private void scheduleThrottledReevaluate() {
+        if (isEvaluatePendingAndNotInProgress()) {
+            logi("scheduleThrottledReevaluate: throttling reevaluate, eval already pending: "
+                    + mScheduledEvaluateFuture);
+        } else {
+            mScheduledEvaluateFuture = mExecutorService.schedule(this::reevaluateDelegates,
+                    mTimerAdapter.getReevaluateThrottleTimerMilliseconds(), TimeUnit.MILLISECONDS);
+            logi("scheduleThrottledReevaluate: new reevaluate scheduled: "
+                    + mScheduledEvaluateFuture);
+        }
+    }
+
+    /**
+     * @return true if there is a evaluate pending, false if there is not. If evaluate has already
+     * begun, but we are waiting for it to complete, this will also return false.
+     */
+    private boolean isEvaluatePendingAndNotInProgress() {
+        boolean isEvalScheduled = mScheduledEvaluateFuture != null
+                && !mScheduledEvaluateFuture.isDone();
+        boolean isEvalInProgress = mEvaluateCompleteFuture != null
+                && !mEvaluateCompleteFuture.isDone();
+        return isEvalScheduled && !isEvalInProgress;
+    }
+
+    /**
+     * Cancel any pending re-evaluates and perform it as soon as possible. This is done in the case
+     * where we need to do something like tear down this controller or change subId.
+     */
+    private void scheduleReevaluateNow(CompletableFuture<Void> onDoneFuture) {
+        if (isEvaluatePendingAndNotInProgress()) {
+            mScheduledEvaluateFuture.cancel(false /*interrupt*/);
+            logi("scheduleReevaluateNow: cancelling pending reevaluate: "
+                    + mScheduledEvaluateFuture);
+        }
+        // we have tasks that depend on this potentially, so once the last reevaluate is done,
+        // schedule a new one right away.
+        if (mEvaluateCompleteFuture != null && !mEvaluateCompleteFuture.isDone()) {
+            mEvaluateCompleteFuture.thenRunAsync(
+                    () -> scheduleReevaluateNow(onDoneFuture), mExecutorService);
+            return;
+        }
+
+        reevaluateDelegates();
+        mEvaluateCompleteFuture.thenAccept(onDoneFuture::complete);
+    }
+
+    /**
+     * Apply all operations that have been pending by collecting pending create/destroy operations
+     * and batch applying them to the mDelegatePriorityQueue.
+     *
+     * First perform the operation of destroying all SipDelegateConnections that have been pending
+     * destroy. Next, add all pending new SipDelegateControllers to the end of
+     * mDelegatePriorityQueue and loop through all in the queue, applying feature tags to the
+     * appropriate SipDelegateController if they pass role checks and have not already been claimed
+     * by another delegate higher in the priority queue.
+     */
+    private void reevaluateDelegates() {
+        if (mEvaluateCompleteFuture != null && !mEvaluateCompleteFuture.isDone()) {
+            logw("reevaluateDelegates: last evaluate not done, deferring new request");
+            // Defer re-evaluate until after the pending re-evaluate is complete.
+            mEvaluateCompleteFuture.thenRunAsync(this::scheduleThrottledReevaluate,
+                    mExecutorService);
+            return;
+        }
+
+        // Destroy all pending destroy delegates first. Order doesn't matter.
+        List<CompletableFuture<Void>> pendingDestroyList = mDelegatePendingDestroy.stream()
+                .map(d -> triggerDestroy(d.controller, d.reason)).collect(
+                Collectors.toList());
+        CompletableFuture<Void> pendingDestroy = CompletableFuture.allOf(
+                pendingDestroyList.toArray(new CompletableFuture[mDelegatePendingDestroy.size()]));
+        mDelegatePriorityQueue.removeAll(mDelegatePendingDestroy.stream().map(d -> d.controller)
+                .collect(Collectors.toList()));
+        mDelegatePendingDestroy.clear();
+
+        // Add newly created SipDelegates to end of queue before evaluating associated features.
+        mDelegatePriorityQueue.addAll(mDelegatePendingCreate);
+        for (SipDelegateController c : mDelegatePendingCreate) {
+            logi("reevaluateDelegates: pending create: " + c);
+        }
+        mDelegatePendingCreate.clear();
+
+        // Wait for destroy stages to complete, then loop from oldest to most recent and associate
+        // feature tags that the app has requested to the SipDelegate.
+        // Each feature tag can only be associated with one SipDelegate, so as feature tags are
+        // taken, do not allow other SipDelegates to be associated with those tags as well. Each
+        // stage of the CompletableFuture chain passes the previously claimed feature tags into the
+        // next stage so that those feature tags can be denied if already claimed.
+        // Executor doesn't matter here, just composing here to transform to the next stage.
+        CompletableFuture<Set<String>> pendingChange = pendingDestroy.thenCompose((ignore) -> {
+            logi("reevaluateDelegates: destroy phase complete");
+            return CompletableFuture.completedFuture(new ArraySet<>());
+        });
+        final String cachedSmsRolePackage = mCachedSmsRolePackageName;
+        for (SipDelegateController c : mDelegatePriorityQueue) {
+            logi("reevaluateDelegates: pending reeval: " + c);
+            pendingChange = pendingChange.thenComposeAsync((takenTags) -> {
+                logi("reevaluateDelegates: last stage completed with result:" + takenTags);
+                if (takenTags == null) {
+                    // return early, the ImsService is no longer available. This will eventually be
+                    // destroyed.
+                    return CompletableFuture.completedFuture(null /*failed*/);
+                }
+                return changeSupportedFeatureTags(c, cachedSmsRolePackage, takenTags);
+            }, mExecutorService);
+        }
+
+        // Executor doesn't matter here, just adding an extra stage to print result.
+        mEvaluateCompleteFuture = pendingChange
+                .thenAccept((associatedFeatures) -> logi("reevaluateDelegates: reevaluate complete,"
+                        + " feature tags associated: " + associatedFeatures));
+        logi("reevaluateDelegates: future created.");
+    }
+
+    private CompletableFuture<Void> triggerDestroy(SipDelegateController c, int reason) {
+        return c.destroy(isForcedFromReason(reason), reason)
+                // Executor doesn't matter here, just for logging.
+                .thenAccept((delegateReason) -> logi("destroy triggered with " + reason
+                        + " and finished with reason= " + delegateReason));
+    }
+
+    private boolean isForcedFromReason(int reason) {
+        switch (reason) {
+            case SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_UNKNOWN:
+                logw("isForcedFromReason, unknown reason");
+                /*intentional fallthrough*/
+            case SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP:
+                /*intentional fallthrough*/
+            case SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_USER_DISABLED_RCS:
+                return false;
+            case SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SERVICE_DEAD:
+                /*intentional fallthrough*/
+            case SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SUBSCRIPTION_TORN_DOWN:
+                return true;
+        }
+        logw("isForcedFromReason, unexpected reason: " + reason);
+        return false;
+    }
+
+    /**
+     * Called by RoleManager when a role has changed so that we can query the new role holder.
+     * @param roleName the name of the role whose holders are changed
+     * @param user the user for this role holder change
+     */
+    // Called on mExecutorThread
+    @Override
+    public void onRoleHoldersChanged(@NonNull String roleName, @NonNull UserHandle user) {
+        logi("onRoleHoldersChanged, roleName= " + roleName + ", user= " + user);
+        // Only monitor changes on the system
+        if (!UserHandle.SYSTEM.equals(user)) {
+            return;
+        }
+
+        if (!RoleManager.ROLE_SMS.equals(roleName)) {
+            logi("onRoleHoldersChanged, ignoring non SMS role change");
+            // TODO: only target default sms app for now and add new roles later using
+            // CarrierConfigManager
+            return;
+        }
+        updateRoleCache();
+        // new denied tags will be picked up when reevaluate completes.
+        scheduleThrottledReevaluate();
+    }
+
+
+    private void updateRoleCache() {
+        String newSmsRolePackageName = "";
+        try {
+            // Only one app can fulfill the SMS role.
+            newSmsRolePackageName = mRoleManagerAdapter.getRoleHolders(RoleManager.ROLE_SMS)
+                    .stream().findFirst().orElse("");
+        } catch (Exception e) {
+            logi("updateRoleCache: exception=" + e);
+        }
+
+        logi("updateRoleCache: new packageName=" + newSmsRolePackageName);
+        if (TextUtils.equals(mCachedSmsRolePackageName, newSmsRolePackageName)) {
+            logi("updateRoleCache, skipping, role did not change");
+            return;
+        }
+        mCachedSmsRolePackageName = newSmsRolePackageName;
+    }
+
+    /**
+     * Check the requested roles for the specified package name and return the tags that were
+     * applied to that SipDelegateController.
+     * @param controller Controller to attribute feature tags to.
+     * @param alreadyRequestedTags The feature tags that were already granted to other SipDelegates.
+     * @return Once complete, contains the set of feature tags that the SipDelegate now has
+     * associated with it along with the feature tags that previous SipDelegates had.
+     *
+     * // TODO: we currently only track SMS role, extend to support other roles as well.
+     */
+    private CompletableFuture<Set<String>> changeSupportedFeatureTags(
+            SipDelegateController controller, String smsRolePackageName,
+            Set<String> alreadyRequestedTags) {
+        Set<String> requestedFeatureTags = controller.getInitialRequest().getFeatureTags();
+        String packageName = controller.getPackageName();
+        if (!smsRolePackageName.equals(packageName)) {
+            // Deny all tags.
+            Set<FeatureTagState> deniedTags = new ArraySet<>();
+            for (String s : requestedFeatureTags) {
+                deniedTags.add(new FeatureTagState(s,
+                        SipDelegateManager.DENIED_REASON_NOT_ALLOWED));
+            }
+            CompletableFuture<Boolean> pendingDeny = controller.changeSupportedFeatureTags(
+                    Collections.emptySet(), deniedTags);
+            logi("changeSupportedFeatureTags pendingDeny=" + pendingDeny);
+            // do not worry about executor used here, this stage used to interpret result + add log.
+            return pendingDeny.thenApply((completedSuccessfully) ->  {
+                logi("changeSupportedFeatureTags: deny completed: " + completedSuccessfully);
+                if (!completedSuccessfully) return null;
+                // Return back the previous list of requested tags, as we did not add any more.
+                return alreadyRequestedTags;
+            });
+        }
+
+        ArraySet<String> previouslyGrantedTags = new ArraySet<>(alreadyRequestedTags);
+        // deny tags already used by other delegates
+        Set<FeatureTagState> deniedTags = new ArraySet<>();
+        for (String s : requestedFeatureTags) {
+            if (previouslyGrantedTags.contains(s)) {
+                deniedTags.add(new FeatureTagState(s,
+                        SipDelegateManager.DENIED_REASON_IN_USE_BY_ANOTHER_DELEGATE));
+            }
+        }
+        Set<String> nonDeniedTags = requestedFeatureTags.stream()
+                .filter(r -> !previouslyGrantedTags.contains(r))
+                .collect(Collectors.toSet());
+        // Add newly granted tags to the already requested tags list.
+        previouslyGrantedTags.addAll(nonDeniedTags);
+        CompletableFuture<Boolean> pendingChange = controller.changeSupportedFeatureTags(
+                nonDeniedTags, deniedTags);
+        logi("changeSupportedFeatureTags pendingChange=" + pendingChange);
+        // do not worry about executor used here, this stage used to interpret result + add log.
+        return pendingChange.thenApply((completedSuccessfully) ->  {
+            logi("changeSupportedFeatureTags: change completed: " + completedSuccessfully);
+            if (!completedSuccessfully) return null;
+            return previouslyGrantedTags;
+        });
+    }
+
     /**
      * Run a Callable on the ExecutorService Thread and wait for the result.
      * If an ImsException is thrown, catch it and rethrow it to caller.
@@ -169,7 +792,31 @@
 
     private void onRcsManagerChanged(RcsFeatureManager m) {
         logi("manager changed, " + mRcsManager + "->" + m);
+        if (mRcsManager == m) return;
         mRcsManager = m;
+        if (mRcsManager == null) {
+            logi("onRcsManagerChanged: lost connection to ImsService, tearing down...");
+            unregisterListeners();
+            scheduleDestroyDelegates(SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SERVICE_DEAD);
+        } else {
+            logi("onRcsManagerChanged: registering listeners/updating role cache...");
+            registerListeners();
+            updateRoleCache();
+        }
+    }
+
+    private void registerListeners() {
+        try {
+            mRoleManagerAdapter.addOnRoleHoldersChangedListenerAsUser(mExecutorService, this,
+                    UserHandle.SYSTEM);
+        } catch (Exception e) {
+            logi("registerListeners: exception=" + e);
+        }
+    }
+
+    private void unregisterListeners() {
+        mCachedSmsRolePackageName = "";
+        mRoleManagerAdapter.removeOnRoleHoldersChangedListenerAsUser(this, UserHandle.SYSTEM);
     }
 
     /**
@@ -178,18 +825,65 @@
      */
     private void onSubIdChanged(int newSubId) {
         logi("subId changed, " + mSubId + "->" + newSubId);
-        mSubId = newSubId;
+        if (mSubId != newSubId) {
+            // Swap subId, any pending create/destroy on old subId will be denied.
+            mSubId = newSubId;
+            scheduleDestroyDelegates(
+                    SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SUBSCRIPTION_TORN_DOWN);
+            return;
+        }
+        // TODO: if subId hasn't changed this means that we should load in any new carrier configs
+        // that we care about and apply.
     }
 
-    private void logi(String message) {
-        Log.i(LOG_TAG, getPrefix() + ": " + message);
+    /**
+     * Destroy all tracked SipDelegateConnections due to the subscription being torn down.
+     * @return A CompletableFuture that will complete when all SipDelegates have been torn down.
+     */
+    private CompletableFuture<Void> scheduleDestroyDelegates(int reason) {
+        boolean addedDestroy = false;
+        for (SipDelegateController c : mDelegatePriorityQueue) {
+            logi("scheduleDestroyDelegates: Controller pending destroy: " + c);
+            addedDestroy |= addPendingDestroy(c, reason);
+        }
+        if (addedDestroy) {
+            CompletableFuture<Void> pendingDestroy = new CompletableFuture<>();
+            scheduleReevaluateNow(pendingDestroy);
+            return pendingDestroy;
+        } else {
+            return CompletableFuture.completedFuture(null);
+        }
     }
 
-    private void logw(String message) {
-        Log.w(LOG_TAG, getPrefix() + ": " + message);
+    @Override
+    public void dump(PrintWriter printWriter) {
+        IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, "  ");
+        pw.println("SipTransportController" + "[" + mSlotId  + "->" + mSubId + "]:");
+        pw.increaseIndent();
+        pw.println("LocalLog:");
+        pw.increaseIndent();
+        mLocalLog.dump(pw);
+        pw.decreaseIndent();
+        pw.println("SipDelegateControllers (in priority order):");
+        pw.increaseIndent();
+        if (mDelegatePriorityQueue.isEmpty()) {
+            pw.println("[NONE]");
+        } else {
+            for (SipDelegateController c : mDelegatePriorityQueue) {
+                c.dump(pw);
+            }
+        }
+        pw.decreaseIndent();
+        pw.decreaseIndent();
     }
 
-    private String getPrefix() {
-        return "[" + mSlotId + "," + mSubId + "]";
+    private void logi(String log) {
+        Log.w(LOG_TAG, "[" + mSlotId  + "->" + mSubId + "] " + log);
+        mLocalLog.log("[I] " + log);
+    }
+
+    private void logw(String log) {
+        Log.w(LOG_TAG, "[" + mSlotId  + "->" + mSubId + "] " + log);
+        mLocalLog.log("[W] " + log);
     }
 }
diff --git a/src/com/android/services/telephony/rcs/TelephonyRcsService.java b/src/com/android/services/telephony/rcs/TelephonyRcsService.java
index 79170af..941a6a8 100644
--- a/src/com/android/services/telephony/rcs/TelephonyRcsService.java
+++ b/src/com/android/services/telephony/rcs/TelephonyRcsService.java
@@ -47,21 +47,19 @@
     private static final String LOG_TAG = "TelephonyRcsService";
 
     /**
-     * Used to inject RcsFeatureController and UserCapabilityExchangeImpl instances for testing.
+     * Used to inject RcsFeatureController and UceController instances for testing.
      */
     @VisibleForTesting
     public interface FeatureFactory {
         /**
-         * @return an {@link RcsFeatureController} assoicated with the slot specified.
+         * @return an {@link RcsFeatureController} associated with the slot specified.
          */
         RcsFeatureController createController(Context context, int slotId);
 
         /**
-         * @return an instance of {@link UserCapabilityExchangeImpl} associated with the slot
-         * specified.
+         * @return an instance of {@link UceControllerManager} associated with the slot specified.
          */
-        UserCapabilityExchangeImpl createUserCapabilityExchange(Context context, int slotId,
-                int subId);
+        UceControllerManager createUceControllerManager(Context context, int slotId, int subId);
 
         /**
          * @return an instance of {@link SipTransportController} for the slot and subscription
@@ -77,9 +75,9 @@
         }
 
         @Override
-        public UserCapabilityExchangeImpl createUserCapabilityExchange(Context context, int slotId,
+        public UceControllerManager createUceControllerManager(Context context, int slotId,
                 int subId) {
-            return new UserCapabilityExchangeImpl(context, slotId, subId);
+            return new UceControllerManager(context, slotId, subId);
         }
 
         @Override
@@ -237,13 +235,13 @@
 
     private void updateSupportedFeatures(RcsFeatureController c, int slotId, int subId) {
         if (doesSubscriptionSupportPresence(subId)) {
-            if (c.getFeature(UserCapabilityExchangeImpl.class) == null) {
-                c.addFeature(mFeatureFactory.createUserCapabilityExchange(mContext, slotId, subId),
-                        UserCapabilityExchangeImpl.class);
+            if (c.getFeature(UceControllerManager.class) == null) {
+                c.addFeature(mFeatureFactory.createUceControllerManager(mContext, slotId, subId),
+                        UceControllerManager.class);
             }
         } else {
-            if (c.getFeature(UserCapabilityExchangeImpl.class) != null) {
-                c.removeFeature(UserCapabilityExchangeImpl.class);
+            if (c.getFeature(UceControllerManager.class) != null) {
+                c.removeFeature(UceControllerManager.class);
             }
         }
 
diff --git a/src/com/android/services/telephony/rcs/UceControllerManager.java b/src/com/android/services/telephony/rcs/UceControllerManager.java
new file mode 100644
index 0000000..d1f91d1
--- /dev/null
+++ b/src/com/android/services/telephony/rcs/UceControllerManager.java
@@ -0,0 +1,247 @@
+/*
+ * 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.net.Uri;
+import android.telephony.ims.ImsException;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.RcsUceAdapter.PublishState;
+import android.telephony.ims.aidl.IRcsUceControllerCallback;
+import android.telephony.ims.aidl.IRcsUcePublishStateCallback;
+import android.util.Log;
+
+import com.android.ims.RcsFeatureManager;
+import com.android.ims.rcs.uce.UceController;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.io.PrintWriter;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+/**
+ * Responsible for managing the creation and destruction of UceController. It also received the
+ * requests from {@link com.android.phone.ImsRcsController} and pass these requests to
+ * {@link UceController}
+ */
+public class UceControllerManager implements RcsFeatureController.Feature {
+
+    private static final String LOG_TAG = "UceControllerManager";
+
+    private final int mSlotId;
+    private final Context mContext;
+    private final ExecutorService mExecutorService;
+
+    private volatile UceController mUceController;
+
+    public UceControllerManager(Context context, int slotId, int subId) {
+        Log.d(LOG_TAG, "create: slotId=" + slotId + ", subId=" + subId);
+
+        mSlotId = slotId;
+        mContext = context;
+        mExecutorService = Executors.newSingleThreadExecutor();
+        mUceController = new UceController(mContext, subId);
+    }
+
+    /**
+     * Constructor to inject dependencies for testing.
+     */
+    @VisibleForTesting
+    public UceControllerManager(Context context, int slotId, int subId, ExecutorService executor) {
+        mSlotId = slotId;
+        mContext = context;
+        mExecutorService = executor;
+        mUceController = new UceController(mContext, subId);
+    }
+
+    @Override
+    public void onRcsConnected(RcsFeatureManager manager) {
+        mExecutorService.submit(() -> mUceController.onRcsConnected(manager));
+    }
+
+    @Override
+    public void onRcsDisconnected() {
+        mExecutorService.submit(() -> mUceController.onRcsDisconnected());
+    }
+
+    @Override
+    public void onDestroy() {
+        Log.d(LOG_TAG, "onDestroy");
+        mExecutorService.submit(() -> mUceController.onDestroy());
+        // When the shutdown is called, it will refuse any new tasks and let existing tasks finish.
+        mExecutorService.shutdown();
+    }
+
+    /**
+     * This method will be called when either the subscription ID associated with the slot has
+     * changed or the carrier configuration associated with the same subId has changed.
+     */
+    @Override
+    public void onAssociatedSubscriptionUpdated(int subId) {
+        mExecutorService.submit(() -> {
+            Log.i(LOG_TAG, "onAssociatedSubscriptionUpdated: slotId=" + mSlotId
+                    + ", subId=" + subId);
+
+            // Destroy existing UceController and create a new one.
+            mUceController.onDestroy();
+            mUceController = new UceController(mContext, subId);
+        });
+    }
+
+    @VisibleForTesting
+    public void setUceController(UceController uceController) {
+        mUceController = uceController;
+    }
+
+    /**
+     * Request the capabilities for contacts.
+     *
+     * @param contactNumbers A list of numbers that the capabilities are being requested for.
+     * @param c A callback for when the request for capabilities completes.
+     * @throws ImsException if the ImsService connected to this controller is currently down.
+     */
+    public void requestCapabilities(List<Uri> contactNumbers, IRcsUceControllerCallback c)
+            throws ImsException {
+        Future future = mExecutorService.submit(() -> {
+            checkUceControllerState();
+            mUceController.requestCapabilities(contactNumbers, c);
+            return true;
+        });
+
+        try {
+            future.get();
+        } catch (ExecutionException | InterruptedException e) {
+            Log.w(LOG_TAG, "requestCapabilities: " + e);
+            Throwable cause = e.getCause();
+            if (cause instanceof ImsException) {
+                throw (ImsException) cause;
+            }
+        }
+    }
+
+    /**
+     * Request the capabilities for the given contact.
+     * @param contactNumber The contact of the capabilities are being requested for.
+     * @param c A callback for when the request for capabilities completes.
+     * @throws ImsException if the ImsService connected to this controller is currently down.
+     */
+    public void requestNetworkAvailability(Uri contactNumber, IRcsUceControllerCallback c)
+            throws ImsException {
+        Future future = mExecutorService.submit(() -> {
+            checkUceControllerState();
+            mUceController.requestAvailability(contactNumber, c);
+            return true;
+        });
+
+        try {
+            future.get();
+        } catch (ExecutionException | InterruptedException e) {
+            Log.w(LOG_TAG, "requestNetworkAvailability exception: " + e);
+            Throwable cause = e.getCause();
+            if (cause instanceof ImsException) {
+                throw (ImsException) cause;
+            }
+        }
+    }
+
+    /**
+     * Get the UCE publish state.
+     *
+     * @throws ImsException if the ImsService connected to this controller is currently down.
+     */
+    public @PublishState int getUcePublishState() throws ImsException {
+        Future future = mExecutorService.submit(() -> {
+            checkUceControllerState();
+            return mUceController.getUcePublishState();
+        });
+
+        try {
+            return (Integer) future.get();
+        } catch (ExecutionException | InterruptedException e) {
+            Log.w(LOG_TAG, "requestNetworkAvailability exception: " + e);
+            Throwable cause = e.getCause();
+            if (cause instanceof ImsException) {
+                throw (ImsException) cause;
+            }
+            return RcsUceAdapter.PUBLISH_STATE_OTHER_ERROR;
+        }
+    }
+
+    /**
+     * Register the Publish state changed callback.
+     *
+     * @throws ImsException if the ImsService connected to this controller is currently down.
+     */
+    public void registerPublishStateCallback(IRcsUcePublishStateCallback c) throws ImsException {
+        Future future = mExecutorService.submit(() -> {
+            checkUceControllerState();
+            mUceController.registerPublishStateCallback(c);
+            return true;
+        });
+
+        try {
+            future.get();
+        } catch (ExecutionException | InterruptedException e) {
+            Log.w(LOG_TAG, "registerPublishStateCallback exception: " + e);
+            Throwable cause = e.getCause();
+            if (cause instanceof ImsException) {
+                throw (ImsException) cause;
+            }
+        }
+    }
+
+    /**
+     * Unregister the existing publish state changed callback.
+     */
+    public void unregisterPublishStateCallback(IRcsUcePublishStateCallback c) {
+        Future future = mExecutorService.submit(() -> {
+            if (checkUceControllerState()) {
+                mUceController.unregisterPublishStateCallback(c);
+            }
+            return true;
+        });
+
+        try {
+            future.get();
+        } catch (ExecutionException | InterruptedException e) {
+            Log.w(LOG_TAG, "unregisterPublishStateCallback exception: " + e);
+        }
+    }
+
+    private boolean checkUceControllerState() throws ImsException {
+        if (mUceController == null || mUceController.isUnavailable()) {
+            throw new ImsException("UCE controller is unavailable",
+                    ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
+        }
+        return true;
+    }
+
+
+    @Override
+    public void dump(PrintWriter printWriter) {
+        IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, "  ");
+        pw.println("UceControllerManager" + "[" + mSlotId + "]:");
+        pw.increaseIndent();
+        pw.println("UceController available = " + mUceController != null);
+        //TODO: Add dump for UceController
+        pw.decreaseIndent();
+    }
+}
diff --git a/src/com/android/services/telephony/rcs/UserCapabilityExchangeImpl.java b/src/com/android/services/telephony/rcs/UserCapabilityExchangeImpl.java
deleted file mode 100644
index ee0c5be..0000000
--- a/src/com/android/services/telephony/rcs/UserCapabilityExchangeImpl.java
+++ /dev/null
@@ -1,1065 +0,0 @@
-/*
- * 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.BroadcastReceiver;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.database.ContentObserver;
-import android.net.Uri;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.Message;
-import android.os.PersistableBundle;
-import android.os.RemoteCallbackList;
-import android.os.RemoteException;
-import android.provider.Settings;
-import android.provider.Telephony;
-import android.telecom.TelecomManager;
-import android.telephony.AccessNetworkConstants;
-import android.telephony.CarrierConfigManager;
-import android.telephony.SubscriptionManager;
-import android.telephony.ims.ImsException;
-import android.telephony.ims.ImsManager;
-import android.telephony.ims.ImsMmTelManager;
-import android.telephony.ims.ImsReasonInfo;
-import android.telephony.ims.ProvisioningManager;
-import android.telephony.ims.RcsContactUceCapability;
-import android.telephony.ims.RcsUceAdapter;
-import android.telephony.ims.RegistrationManager;
-import android.telephony.ims.aidl.IRcsUceControllerCallback;
-import android.telephony.ims.aidl.IRcsUcePublishStateCallback;
-import android.telephony.ims.feature.MmTelFeature;
-import android.telephony.ims.stub.RcsCapabilityExchange;
-import android.telephony.ims.stub.RcsPresenceExchangeImplBase;
-import android.util.Log;
-
-import com.android.ims.RcsFeatureManager;
-import com.android.ims.RcsFeatureManager.RcsFeatureCallbacks;
-import com.android.ims.ResultCode;
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.phone.R;
-import com.android.service.ims.presence.ContactCapabilityResponse;
-import com.android.service.ims.presence.PresenceBase;
-import com.android.service.ims.presence.PresencePublication;
-import com.android.service.ims.presence.PresencePublisher;
-import com.android.service.ims.presence.PresenceSubscriber;
-import com.android.service.ims.presence.SubscribePublisher;
-
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.stream.Collectors;
-
-/**
- * Implements User Capability Exchange using Presence.
- */
-public class UserCapabilityExchangeImpl implements RcsFeatureController.Feature, SubscribePublisher,
-        PresencePublisher {
-
-    private static final String LOG_TAG = "RcsUceImpl";
-
-    private final int mSlotId;
-    private volatile int mSubId;
-    private volatile boolean mImsContentChangedCallbackRegistered = false;
-    // The result of requesting publish
-    private volatile int mPublishState = PresenceBase.PUBLISH_STATE_NOT_PUBLISHED;
-    // The network type which IMS registers on
-    private volatile int mNetworkRegistrationType = AccessNetworkConstants.TRANSPORT_TYPE_INVALID;
-    // The MMTel capabilities of this subscription Id
-    private MmTelFeature.MmTelCapabilities mMmTelCapabilities;
-    private final Object mCapabilitiesLock = new Object();
-
-    private final Context mContext;
-    private final UceImplHandler mUceImplHandler;
-    private RcsFeatureManager mRcsFeatureManager;
-    private final PresencePublication mPresencePublication;
-    private final PresenceSubscriber mPresenceSubscriber;
-
-    // The task Ids of updating capabilities
-    private final Set<Integer> mRequestingPublishTaskIds = new HashSet<>();
-
-    // The callbacks to notify publish state changed.
-    private final RemoteCallbackList<IRcsUcePublishStateCallback> mPublishStateCallbacks;
-
-    // The task Ids of pending availability request.
-    private final Set<Integer> mPendingAvailabilityRequests = new HashSet<>();
-
-    private final ConcurrentHashMap<Integer, IRcsUceControllerCallback> mPendingCapabilityRequests =
-            new ConcurrentHashMap<>();
-
-    UserCapabilityExchangeImpl(Context context, int slotId, int subId) {
-        mSlotId = slotId;
-        mSubId = subId;
-        logi("created");
-
-        mContext = context;
-        mPublishStateCallbacks = new RemoteCallbackList<>();
-
-        HandlerThread handlerThread = new HandlerThread("UceImplHandlerThread");
-        handlerThread.start();
-        mUceImplHandler = new UceImplHandler(this, handlerThread.getLooper());
-
-        String[] volteError = context.getResources().getStringArray(
-                R.array.config_volte_provision_error_on_publish_response);
-        String[] rcsError = context.getResources().getStringArray(
-                R.array.config_rcs_provision_error_on_publish_response);
-
-        // Initialize PresencePublication
-        mPresencePublication = new PresencePublication(null /*PresencePublisher*/, context,
-                volteError, rcsError);
-        // Initialize PresenceSubscriber
-        mPresenceSubscriber = new PresenceSubscriber(null /*SubscribePublisher*/, context,
-                volteError, rcsError);
-
-        onAssociatedSubscriptionUpdated(mSubId);
-        registerReceivers();
-    }
-
-    @VisibleForTesting
-    UserCapabilityExchangeImpl(Context context, int slotId, int subId, Looper looper,
-            PresencePublication presencePublication, PresenceSubscriber presenceSubscriber,
-            RemoteCallbackList<IRcsUcePublishStateCallback> publishStateCallbacks) {
-        mSlotId = slotId;
-        mSubId = subId;
-        mContext = context;
-        mPublishStateCallbacks = publishStateCallbacks;
-        mUceImplHandler = new UceImplHandler(this, looper);
-        mPresencePublication = presencePublication;
-        mPresenceSubscriber = presenceSubscriber;
-        onAssociatedSubscriptionUpdated(mSubId);
-        registerReceivers();
-    }
-
-    // Runs on main thread.
-    @Override
-    public void onRcsConnected(RcsFeatureManager rcsFeatureManager) {
-        logi("onRcsConnected");
-        mRcsFeatureManager = rcsFeatureManager;
-        mRcsFeatureManager.addFeatureListenerCallback(mRcsFeatureCallback);
-
-        mPresencePublication.updatePresencePublisher(this);
-        mPresenceSubscriber.updatePresenceSubscriber(this);
-    }
-
-    // Runs on main thread.
-    @Override
-    public void onRcsDisconnected() {
-        logi("onRcsDisconnected");
-        mPresencePublication.removePresencePublisher();
-        mPresenceSubscriber.removePresenceSubscriber();
-
-        if (mRcsFeatureManager != null) {
-            mRcsFeatureManager.releaseConnection();
-            mRcsFeatureManager = null;
-        }
-    }
-
-    // Runs on main thread.
-    @Override
-    public void onAssociatedSubscriptionUpdated(int subId) {
-        logi("onAssociatedSubscriptionUpdated: new subId=" + subId);
-
-        // Listen to the IMS content changed with new subId.
-        mUceImplHandler.registerImsContentChangedReceiver(subId);
-
-        mSubId = subId;
-        mPresencePublication.handleAssociatedSubscriptionChanged(subId);
-        mPresenceSubscriber.handleAssociatedSubscriptionChanged(subId);
-    }
-
-    /**
-     * Should be called before destroying this instance.
-     * This instance is not usable after this method is called.
-     */
-    // Called on main thread.
-    public void onDestroy() {
-        logi("onDestroy");
-        mUceImplHandler.getLooper().quit();
-        unregisterReceivers();
-        unregisterImsProvisionCallback(mSubId);
-        onRcsDisconnected();
-    }
-
-    /**
-     * @return the UCE Publish state.
-     */
-    // May happen on a Binder thread, PresencePublication locks to get result.
-    public int getUcePublishState() {
-        int publishState = mPresencePublication.getPublishState();
-        return toUcePublishState(publishState);
-    }
-
-    @VisibleForTesting
-    public UceImplHandler getHandler() {
-        return mUceImplHandler;
-    }
-
-    /**
-     * Register receiver to receive UCE publish state changed.
-     */
-    public void registerPublishStateCallback(IRcsUcePublishStateCallback c) {
-        synchronized (mPublishStateCallbacks) {
-            mPublishStateCallbacks.register(c);
-        }
-    }
-
-    /**
-     * Unregister UCE publish state callback.
-     */
-    public void unregisterUcePublishStateCallback(IRcsUcePublishStateCallback c) {
-        synchronized (mPublishStateCallbacks) {
-            mPublishStateCallbacks.unregister(c);
-        }
-    }
-
-    private void clearPublishStateCallbacks() {
-        synchronized (mPublishStateCallbacks) {
-            logi("clearPublishStateCallbacks");
-            final int lastIndex = mPublishStateCallbacks.getRegisteredCallbackCount() - 1;
-            for (int index = lastIndex; index >= 0; index--) {
-                IRcsUcePublishStateCallback callback =
-                        mPublishStateCallbacks.getRegisteredCallbackItem(index);
-                mPublishStateCallbacks.unregister(callback);
-            }
-        }
-    }
-
-    private void notifyPublishStateChanged(@PresenceBase.PresencePublishState int state) {
-        int result = toUcePublishState(state);
-        synchronized (mPublishStateCallbacks) {
-            mPublishStateCallbacks.broadcast(c -> {
-                try {
-                    c.onPublishStateChanged(result);
-                } catch (RemoteException e) {
-                    logw("notifyPublishStateChanged error: " + e);
-                }
-            });
-        }
-    }
-
-    /**
-     * Perform a capabilities request and call {@link IRcsUceControllerCallback} with the result.
-     */
-    // May happen on a Binder thread, PresenceSubscriber locks when requesting Capabilities.
-    public void requestCapabilities(List<Uri> contactNumbers, IRcsUceControllerCallback c) {
-        List<String> numbers = contactNumbers.stream()
-                .map(UserCapabilityExchangeImpl::getNumberFromUri).collect(Collectors.toList());
-        int taskId = mPresenceSubscriber.requestCapability(numbers,
-                new ContactCapabilityResponse() {
-                    @Override
-                    public void onSuccess(int reqId) {
-                        logi("onSuccess called for reqId:" + reqId);
-                    }
-
-                    @Override
-                    public void onError(int reqId, int resultCode) {
-                        IRcsUceControllerCallback c = mPendingCapabilityRequests.remove(reqId);
-                        try {
-                            if (c != null) {
-                                c.onError(toUceError(resultCode));
-                            } else {
-                                logw("onError called for unknown reqId:" + reqId);
-                            }
-                        } catch (RemoteException e) {
-                            logi("Calling back to dead service");
-                        }
-                    }
-
-                    @Override
-                    public void onFinish(int reqId) {
-                        logi("onFinish called for reqId:" + reqId);
-                    }
-
-                    @Override
-                    public void onTimeout(int reqId) {
-                        IRcsUceControllerCallback c = mPendingCapabilityRequests.remove(reqId);
-                        try {
-                            if (c != null) {
-                                c.onError(RcsUceAdapter.ERROR_REQUEST_TIMEOUT);
-                            } else {
-                                logw("onTimeout called for unknown reqId:" + reqId);
-                            }
-                        } catch (RemoteException e) {
-                            logi("Calling back to dead service");
-                        }
-                    }
-
-                    @Override
-                    public void onCapabilitiesUpdated(int reqId,
-                            List<RcsContactUceCapability> contactCapabilities,
-                            boolean updateLastTimestamp) {
-                        IRcsUceControllerCallback c = mPendingCapabilityRequests.remove(reqId);
-                        try {
-                            if (c != null) {
-                                c.onCapabilitiesReceived(contactCapabilities);
-                            } else {
-                                logw("onCapabilitiesUpdated, unknown reqId:" + reqId);
-                            }
-                        } catch (RemoteException e) {
-                            logw("onCapabilitiesUpdated on dead service");
-                        }
-                    }
-                });
-        if (taskId < 0) {
-            try {
-                c.onError(toUceError(taskId));
-            } catch (RemoteException e) {
-                logi("Calling back to dead service");
-            }
-            return;
-        }
-        mPendingCapabilityRequests.put(taskId, c);
-    }
-
-    @Override
-    public int requestCapability(String[] formattedContacts, int taskId) {
-        if (formattedContacts == null || formattedContacts.length == 0) {
-            logw("requestCapability error: contacts is null.");
-            return ResultCode.SUBSCRIBE_INVALID_PARAM;
-        }
-        if (mRcsFeatureManager == null) {
-            logw("requestCapability error: RcsFeatureManager is null.");
-            return ResultCode.ERROR_SERVICE_NOT_AVAILABLE;
-        }
-
-        logi("requestCapability: taskId=" + taskId);
-
-        try {
-            List<Uri> contactList = Arrays.stream(formattedContacts)
-                    .map(Uri::parse).collect(Collectors.toList());
-            mRcsFeatureManager.requestCapabilities(contactList, taskId);
-        } catch (Exception e) {
-            logw("requestCapability error: " + e.getMessage());
-            return ResultCode.ERROR_SERVICE_NOT_AVAILABLE;
-        }
-        return ResultCode.SUCCESS;
-    }
-
-    @Override
-    public int requestAvailability(String formattedContact, int taskId) {
-        if (formattedContact == null || formattedContact.isEmpty()) {
-            logw("requestAvailability error: contact is null.");
-            return ResultCode.SUBSCRIBE_INVALID_PARAM;
-        }
-        if (mRcsFeatureManager == null) {
-            logw("requestAvailability error: RcsFeatureManager is null.");
-            return ResultCode.ERROR_SERVICE_NOT_AVAILABLE;
-        }
-
-        logi("requestAvailability: taskId=" + taskId);
-        addRequestingAvailabilityTaskId(taskId);
-
-        try {
-            Uri contactUri = Uri.parse(formattedContact);
-            List<Uri> contactUris = new ArrayList<>(Arrays.asList(contactUri));
-            mRcsFeatureManager.requestCapabilities(contactUris, taskId);
-        } catch (Exception e) {
-            logw("requestAvailability error: " + e.getMessage());
-            removeRequestingAvailabilityTaskId(taskId);
-            return ResultCode.ERROR_SERVICE_NOT_AVAILABLE;
-        }
-        return ResultCode.SUCCESS;
-    }
-
-    @Override
-    public int getStackStatusForCapabilityRequest() {
-        if (mRcsFeatureManager == null) {
-            logw("Check Stack status: Error! RcsFeatureManager is null.");
-            return ResultCode.ERROR_SERVICE_NOT_AVAILABLE;
-        }
-
-        if (!isCapabilityDiscoveryEnabled(mSubId)) {
-            logw("Check Stack status: Error! capability discovery not enabled");
-            return ResultCode.ERROR_SERVICE_NOT_ENABLED;
-        }
-
-        if (!isEabProvisioned(mContext, mSubId)) {
-            logw("Check Stack status: Error! EAB provisioning disabled.");
-            return ResultCode.ERROR_SERVICE_NOT_ENABLED;
-        }
-
-        if (getPublisherState() != PresenceBase.PUBLISH_STATE_200_OK) {
-            logw("Check Stack status: Error! publish state " + getPublisherState());
-            return ResultCode.ERROR_SERVICE_NOT_PUBLISHED;
-        }
-        return ResultCode.SUCCESS;
-    }
-
-    /**
-     * The feature callback is to receive the request and update from RcsPresExchangeImplBase
-     */
-    @VisibleForTesting
-    public RcsFeatureCallbacks mRcsFeatureCallback = new RcsFeatureCallbacks() {
-        public void onCommandUpdate(int commandCode, int operationToken) {
-            logi("onCommandUpdate: code=" + commandCode + ", token=" + operationToken);
-            if (isPublishRequestExisted(operationToken)) {
-                onCommandUpdateForPublishRequest(commandCode, operationToken);
-            } else if (isCapabilityRequestExisted(operationToken)) {
-                onCommandUpdateForCapabilityRequest(commandCode, operationToken);
-            } else if (isAvailabilityRequestExisted(operationToken)) {
-                onCommandUpdateForAvailabilityRequest(commandCode, operationToken);
-            } else {
-                logw("onCommandUpdate: invalid token " + operationToken);
-            }
-        }
-
-        /** See {@link RcsPresenceExchangeImplBase#onNetworkResponse(int, String, int)} */
-        public void onNetworkResponse(int responseCode, String reason, int operationToken) {
-            logi("onNetworkResponse: code=" + responseCode + ", reason=" + reason
-                    + ", operationToken=" + operationToken);
-            if (isPublishRequestExisted(operationToken)) {
-                onNetworkResponseForPublishRequest(responseCode, reason, operationToken);
-            } else if (isCapabilityRequestExisted(operationToken)) {
-                onNetworkResponseForCapabilityRequest(responseCode, reason, operationToken);
-            } else if (isAvailabilityRequestExisted(operationToken)) {
-                onNetworkResponseForAvailabilityRequest(responseCode, reason, operationToken);
-            } else {
-                logw("onNetworkResponse: invalid token " + operationToken);
-            }
-        }
-
-        /** See {@link RcsPresenceExchangeImplBase#onCapabilityRequestResponse(List, int)} */
-        public void onCapabilityRequestResponsePresence(List<RcsContactUceCapability> infos,
-                int operationToken) {
-            if (isAvailabilityRequestExisted(operationToken)) {
-                handleAvailabilityReqResponse(infos, operationToken);
-            } else if (isCapabilityRequestExisted(operationToken)) {
-                handleCapabilityReqResponse(infos, operationToken);
-            } else {
-                logw("capability request response: invalid token " + operationToken);
-            }
-        }
-
-        /** See {@link RcsPresenceExchangeImplBase#onNotifyUpdateCapabilites(int)} */
-        public void onNotifyUpdateCapabilities(int publishTriggerType) {
-            logi("onNotifyUpdateCapabilities: type=" + publishTriggerType);
-            mUceImplHandler.notifyUpdateCapabilities(publishTriggerType);
-        }
-
-        /** See {@link RcsPresenceExchangeImplBase#onUnpublish()} */
-        public void onUnpublish() {
-            logi("onUnpublish");
-            mUceImplHandler.unpublish();
-        }
-    };
-
-    private static class UceImplHandler extends Handler {
-        private static final int EVENT_REGISTER_IMS_CHANGED_RECEIVER = 1;
-        private static final int EVENT_NOTIFY_UPDATE_CAPABILITIES = 2;
-        private static final int EVENT_UNPUBLISH = 3;
-
-        private static final int REGISTER_IMS_CHANGED_DELAY = 10000;  //10 seconds
-
-        private final WeakReference<UserCapabilityExchangeImpl> mUceImplRef;
-
-        UceImplHandler(UserCapabilityExchangeImpl uceImpl, Looper looper) {
-            super(looper);
-            mUceImplRef = new WeakReference(uceImpl);
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            UserCapabilityExchangeImpl uceImpl = mUceImplRef.get();
-            if (uceImpl == null) {
-                return;
-            }
-            switch (msg.what) {
-                case EVENT_REGISTER_IMS_CHANGED_RECEIVER:
-                    int subId = msg.arg1;
-                    uceImpl.registerImsContentChangedReceiverInternal(subId);
-                    break;
-                case EVENT_NOTIFY_UPDATE_CAPABILITIES:
-                    int publishTriggerType = msg.arg1;
-                    uceImpl.onNotifyUpdateCapabilities(publishTriggerType);
-                    break;
-                case EVENT_UNPUBLISH:
-                    uceImpl.onUnPublish();
-                    break;
-                default:
-                    Log.w(LOG_TAG, "handleMessage: error=" + msg.what);
-                    break;
-            }
-        }
-
-        private void retryRegisteringImsContentChangedReceiver(int subId) {
-            sendRegisteringImsContentChangedMessage(subId, REGISTER_IMS_CHANGED_DELAY);
-        }
-
-        private void registerImsContentChangedReceiver(int subId) {
-            sendRegisteringImsContentChangedMessage(subId, 0);
-        }
-
-        private void sendRegisteringImsContentChangedMessage(int subId, int delay) {
-            if (subId <= SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
-                return;
-            }
-            removeRegisteringImsContentChangedReceiver();
-            Message message = obtainMessage(EVENT_REGISTER_IMS_CHANGED_RECEIVER);
-            message.arg1 = subId;
-            sendMessageDelayed(message, delay);
-        }
-
-        private void removeRegisteringImsContentChangedReceiver() {
-            removeMessages(EVENT_REGISTER_IMS_CHANGED_RECEIVER);
-        }
-
-        private void notifyUpdateCapabilities(int publishTriggerType) {
-            Message message = obtainMessage(EVENT_NOTIFY_UPDATE_CAPABILITIES);
-            message.arg1 = publishTriggerType;
-            sendMessage(message);
-        }
-
-        private void unpublish() {
-            sendEmptyMessage(EVENT_UNPUBLISH);
-        }
-    }
-
-    private void onNotifyUpdateCapabilities(int publishTriggerType) {
-        mPresencePublication.onStackPublishRequested(publishTriggerType);
-    }
-
-    private void onUnPublish() {
-        mPresencePublication.setPublishState(PresenceBase.PUBLISH_STATE_NOT_PUBLISHED);
-    }
-
-    @Override
-    public @PresenceBase.PresencePublishState int getPublisherState() {
-        return mPublishState;
-    }
-
-    @Override
-    public int requestPublication(RcsContactUceCapability capabilities, String contactUri,
-            int taskId) {
-        if (mRcsFeatureManager == null) {
-            logw("requestPublication error: RcsFeatureManager is null.");
-            return ResultCode.ERROR_SERVICE_NOT_AVAILABLE;
-        }
-
-        logi("requestPublication: taskId=" + taskId);
-        addPublishRequestTaskId(taskId);
-
-        try {
-            mRcsFeatureManager.requestPublication(capabilities, taskId);
-        } catch (Exception ex) {
-            logw("requestPublication error: " + ex.getMessage());
-            removePublishRequestTaskId(taskId);
-            return ResultCode.PUBLISH_GENERIC_FAILURE;
-        }
-        return ResultCode.SUCCESS;
-    }
-
-    /*
-     * Handle the callback method RcsFeatureCallbacks#onCommandUpdate(int, int)
-     */
-    private void onCommandUpdateForPublishRequest(int commandCode, int operationToken) {
-        if (!isPublishRequestExisted(operationToken)) {
-            return;
-        }
-        int resultCode = ResultCode.SUCCESS;
-        if (commandCode != RcsCapabilityExchange.COMMAND_CODE_SUCCESS) {
-            logw("onCommandUpdateForPublishRequest failed! taskId=" + operationToken
-                    + ", code=" + commandCode);
-            removePublishRequestTaskId(operationToken);
-            resultCode = ResultCode.PUBLISH_GENERIC_FAILURE;
-        }
-        mPresencePublication.onCommandStatusUpdated(operationToken, operationToken, resultCode);
-    }
-
-    private void onCommandUpdateForCapabilityRequest(int commandCode, int operationToken) {
-        if (!isCapabilityRequestExisted(operationToken)) {
-            return;
-        }
-        int resultCode = ResultCode.SUCCESS;
-        if (commandCode != RcsCapabilityExchange.COMMAND_CODE_SUCCESS) {
-            logw("onCommandUpdateForCapabilityRequest failed! taskId=" + operationToken
-                    + ", code=" + commandCode);
-            mPendingCapabilityRequests.remove(operationToken);
-            resultCode = ResultCode.PUBLISH_GENERIC_FAILURE;
-        }
-        mPresenceSubscriber.onCommandStatusUpdated(operationToken, operationToken, resultCode);
-    }
-
-    private void onCommandUpdateForAvailabilityRequest(int commandCode, int operationToken) {
-        if (!isAvailabilityRequestExisted(operationToken)) {
-            return;
-        }
-        int resultCode = ResultCode.SUCCESS;
-        if (commandCode != RcsCapabilityExchange.COMMAND_CODE_SUCCESS) {
-            logw("onCommandUpdateForAvailabilityRequest failed! taskId=" + operationToken
-                    + ", code=" + commandCode);
-            removeRequestingAvailabilityTaskId(operationToken);
-            resultCode = ResultCode.PUBLISH_GENERIC_FAILURE;
-        }
-        mPresenceSubscriber.onCommandStatusUpdated(operationToken, operationToken, resultCode);
-    }
-
-    /*
-     * Handle the callback method RcsFeatureCallbacks#onNetworkResponse(int, String, int)
-     */
-    private void onNetworkResponseForPublishRequest(int responseCode, String reason,
-            int operationToken) {
-        if (!isPublishRequestExisted(operationToken)) {
-            return;
-        }
-        removePublishRequestTaskId(operationToken);
-        mPresencePublication.onSipResponse(operationToken, responseCode, reason);
-    }
-
-    private void onNetworkResponseForCapabilityRequest(int responseCode, String reason,
-            int operationToken) {
-        if (!isCapabilityRequestExisted(operationToken)) {
-            return;
-        }
-        mPresenceSubscriber.onSipResponse(operationToken, responseCode, reason);
-    }
-
-    private void onNetworkResponseForAvailabilityRequest(int responseCode, String reason,
-            int operationToken) {
-        if (!isAvailabilityRequestExisted(operationToken)) {
-            return;
-        }
-        removeRequestingAvailabilityTaskId(operationToken);
-        mPresenceSubscriber.onSipResponse(operationToken, responseCode, reason);
-    }
-
-    private void handleAvailabilityReqResponse(List<RcsContactUceCapability> infos, int token) {
-        try {
-            if (infos == null || infos.isEmpty()) {
-                logw("handle availability request response: infos is null " + token);
-                return;
-            }
-            logi("handleAvailabilityReqResponse: token=" + token);
-            mPresenceSubscriber.updatePresence(infos.get(0));
-        } finally {
-            removeRequestingAvailabilityTaskId(token);
-        }
-    }
-
-    private void handleCapabilityReqResponse(List<RcsContactUceCapability> infos, int token) {
-        if (infos == null) {
-            logw("handleCapabilityReqResponse: infos is null " + token);
-            mPendingCapabilityRequests.remove(token);
-            return;
-        }
-        logi("handleCapabilityReqResponse: token=" + token);
-        mPresenceSubscriber.updatePresences(token, infos, true, null);
-    }
-
-    @Override
-    public void updatePublisherState(@PresenceBase.PresencePublishState int publishState) {
-        logi("updatePublisherState: from " + mPublishState + " to " + publishState);
-        mPublishState = publishState;
-        notifyPublishStateChanged(publishState);
-    }
-
-    private void addPublishRequestTaskId(int taskId) {
-        synchronized (mRequestingPublishTaskIds) {
-            mRequestingPublishTaskIds.add(taskId);
-        }
-    }
-
-    private void removePublishRequestTaskId(int taskId) {
-        synchronized (mRequestingPublishTaskIds) {
-            mRequestingPublishTaskIds.remove(taskId);
-        }
-    }
-
-    private boolean isPublishRequestExisted(Integer taskId) {
-        synchronized (mRequestingPublishTaskIds) {
-            return mRequestingPublishTaskIds.contains(taskId);
-        }
-    }
-
-    private void addRequestingAvailabilityTaskId(int taskId) {
-        synchronized (mPendingAvailabilityRequests) {
-            mPendingAvailabilityRequests.contains(taskId);
-        }
-    }
-
-    private void removeRequestingAvailabilityTaskId(int taskId) {
-        synchronized (mPendingAvailabilityRequests) {
-            mPendingAvailabilityRequests.remove(taskId);
-        }
-    }
-
-    private boolean isAvailabilityRequestExisted(Integer taskId) {
-        synchronized (mPendingAvailabilityRequests) {
-            return mPendingAvailabilityRequests.contains(taskId);
-        }
-    }
-
-    private boolean isCapabilityRequestExisted(Integer taskId) {
-        return mPendingCapabilityRequests.containsKey(taskId);
-    }
-
-    private static String getNumberFromUri(Uri uri) {
-        String number = uri.getSchemeSpecificPart();
-        String[] numberParts = number.split("[@;:]");
-
-        if (numberParts.length == 0) {
-            return null;
-        }
-        return numberParts[0];
-    }
-
-    private static int toUcePublishState(int publishState) {
-        switch (publishState) {
-            case PresenceBase.PUBLISH_STATE_200_OK:
-                return RcsUceAdapter.PUBLISH_STATE_OK;
-            case PresenceBase.PUBLISH_STATE_NOT_PUBLISHED:
-                return RcsUceAdapter.PUBLISH_STATE_NOT_PUBLISHED;
-            case PresenceBase.PUBLISH_STATE_VOLTE_PROVISION_ERROR:
-                return RcsUceAdapter.PUBLISH_STATE_VOLTE_PROVISION_ERROR;
-            case PresenceBase.PUBLISH_STATE_RCS_PROVISION_ERROR:
-                return RcsUceAdapter.PUBLISH_STATE_RCS_PROVISION_ERROR;
-            case PresenceBase.PUBLISH_STATE_REQUEST_TIMEOUT:
-                return RcsUceAdapter.PUBLISH_STATE_REQUEST_TIMEOUT;
-            case PresenceBase.PUBLISH_STATE_OTHER_ERROR:
-                return RcsUceAdapter.PUBLISH_STATE_OTHER_ERROR;
-            default:
-                return RcsUceAdapter.PUBLISH_STATE_OTHER_ERROR;
-        }
-    }
-
-    private static int toUceError(int resultCode) {
-        switch (resultCode) {
-            case ResultCode.SUBSCRIBE_NOT_REGISTERED:
-                return RcsUceAdapter.ERROR_NOT_REGISTERED;
-            case ResultCode.SUBSCRIBE_REQUEST_TIMEOUT:
-                return RcsUceAdapter.ERROR_REQUEST_TIMEOUT;
-            case ResultCode.SUBSCRIBE_FORBIDDEN:
-                return RcsUceAdapter.ERROR_FORBIDDEN;
-            case ResultCode.SUBSCRIBE_NOT_FOUND:
-                return RcsUceAdapter.ERROR_NOT_FOUND;
-            case ResultCode.SUBSCRIBE_TOO_LARGE:
-                return RcsUceAdapter.ERROR_REQUEST_TOO_LARGE;
-            case ResultCode.SUBSCRIBE_INSUFFICIENT_MEMORY:
-                return RcsUceAdapter.ERROR_INSUFFICIENT_MEMORY;
-            case ResultCode.SUBSCRIBE_LOST_NETWORK:
-                return RcsUceAdapter.ERROR_LOST_NETWORK;
-            case ResultCode.SUBSCRIBE_ALREADY_IN_QUEUE:
-                return RcsUceAdapter.ERROR_ALREADY_IN_QUEUE;
-            default:
-                return RcsUceAdapter.ERROR_GENERIC_FAILURE;
-        }
-    }
-
-    /*
-     * Register receivers for updating capabilities
-     */
-    private void registerReceivers() {
-        IntentFilter filter = new IntentFilter(TelecomManager.ACTION_TTY_PREFERRED_MODE_CHANGED);
-        filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
-        mContext.registerReceiver(mReceiver, filter);
-
-        ContentResolver resolver = mContext.getContentResolver();
-        if (resolver != null) {
-            // Register mobile data content changed.
-            resolver.registerContentObserver(
-                    Settings.Global.getUriFor(Settings.Global.MOBILE_DATA), false,
-                    mMobileDataObserver);
-
-            // Register SIM info content changed.
-            resolver.registerContentObserver(Telephony.SimInfo.CONTENT_URI, false,
-                    mSimInfoContentObserver);
-        }
-    }
-
-    private void unregisterReceivers() {
-        mContext.unregisterReceiver(mReceiver);
-        ContentResolver resolver = mContext.getContentResolver();
-        if (resolver != null) {
-            resolver.unregisterContentObserver(mMobileDataObserver);
-            resolver.unregisterContentObserver(mSimInfoContentObserver);
-        }
-    }
-
-    /**
-     * Register IMS and provision content changed.
-     *
-     * Call the UceImplHandler#registerImsContentChangedReceiver instead of
-     * calling this method directly.
-     */
-    private void registerImsContentChangedReceiverInternal(int subId) {
-        mUceImplHandler.removeRegisteringImsContentChangedReceiver();
-        try {
-            final int originalSubId = mSubId;
-            if ((originalSubId == subId) && (mImsContentChangedCallbackRegistered)) {
-                logi("registerImsContentChangedReceiverInternal: already registered. skip");
-                return;
-            }
-            // Unregister original IMS and Provision callback
-            unregisterImsProvisionCallback(originalSubId);
-            // Register new IMS and Provision callback
-            registerImsProvisionCallback(subId);
-        } catch (ImsException e) {
-            logw("registerImsContentChangedReceiverInternal error: " + e);
-            mUceImplHandler.retryRegisteringImsContentChangedReceiver(subId);
-        }
-    }
-
-    private void unregisterImsProvisionCallback(int subId) {
-        if (subId <= SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
-            return;
-        }
-        // Unregister IMS callback
-        ImsMmTelManager imsMmtelManager = getImsMmTelManager(subId);
-        if (imsMmtelManager != null) {
-            try {
-                imsMmtelManager.unregisterImsRegistrationCallback(mImsRegistrationCallback);
-                imsMmtelManager.unregisterMmTelCapabilityCallback(mCapabilityCallback);
-            } catch (RuntimeException e) {
-                logw("unregister IMS callback error: " + e.getMessage());
-            }
-        }
-
-        // Unregister provision changed callback
-        ProvisioningManager provisioningManager =
-                ProvisioningManager.createForSubscriptionId(subId);
-        try {
-            provisioningManager.unregisterProvisioningChangedCallback(mProvisioningChangedCallback);
-        } catch (RuntimeException e) {
-            logw("unregister provisioning callback error: " + e.getMessage());
-        }
-
-        // Remove all publish state callbacks
-        clearPublishStateCallbacks();
-
-        mImsContentChangedCallbackRegistered = false;
-    }
-
-    private void registerImsProvisionCallback(int subId) throws ImsException {
-        if (subId <= SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
-            return;
-        }
-        // Register IMS callback
-        ImsMmTelManager imsMmtelManager = getImsMmTelManager(subId);
-        if (imsMmtelManager != null) {
-            imsMmtelManager.registerImsRegistrationCallback(mContext.getMainExecutor(),
-                    mImsRegistrationCallback);
-            imsMmtelManager.registerMmTelCapabilityCallback(mContext.getMainExecutor(),
-                    mCapabilityCallback);
-        }
-        // Register provision changed callback
-        ProvisioningManager provisioningManager =
-                ProvisioningManager.createForSubscriptionId(subId);
-        provisioningManager.registerProvisioningChangedCallback(mContext.getMainExecutor(),
-                mProvisioningChangedCallback);
-
-        mImsContentChangedCallbackRegistered = true;
-        logi("registerImsProvisionCallback");
-    }
-
-    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            if (intent == null) return;
-            switch (intent.getAction()) {
-                case TelecomManager.ACTION_TTY_PREFERRED_MODE_CHANGED:
-                    int preferredMode = intent.getIntExtra(
-                            TelecomManager.EXTRA_TTY_PREFERRED_MODE, TelecomManager.TTY_MODE_OFF);
-                    logi("TTY preferred mode changed: " + preferredMode);
-                    mPresencePublication.onTtyPreferredModeChanged(preferredMode);
-                    break;
-
-                case Intent.ACTION_AIRPLANE_MODE_CHANGED:
-                    boolean airplaneMode = intent.getBooleanExtra("state", false);
-                    logi("Airplane mode changed: " + airplaneMode);
-                    mPresencePublication.onAirplaneModeChanged(airplaneMode);
-                    break;
-            }
-        }
-    };
-
-    private ContentObserver mMobileDataObserver = new ContentObserver(
-            new Handler(Looper.getMainLooper())) {
-        @Override
-        public void onChange(boolean selfChange) {
-            boolean isEnabled = Settings.Global.getInt(mContext.getContentResolver(),
-                    Settings.Global.MOBILE_DATA, 1) == 1;
-            logi("Mobile data changed: enabled=" + isEnabled);
-            mPresencePublication.onMobileDataChanged(isEnabled);
-        }
-    };
-
-    private ContentObserver mSimInfoContentObserver = new ContentObserver(
-            new Handler(Looper.getMainLooper())) {
-        @Override
-        public void onChange(boolean selfChange) {
-            if (mSubId <= SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
-                return;
-            }
-
-            ImsMmTelManager ims = getImsMmTelManager(mSubId);
-            if (ims == null) return;
-
-            try {
-                boolean isEnabled = ims.isVtSettingEnabled();
-                logi("SimInfo changed: VT setting=" + isEnabled);
-                mPresencePublication.onVtEnabled(isEnabled);
-            } catch (RuntimeException e) {
-                logw("SimInfo changed error: " + e);
-            }
-        }
-    };
-
-    private RegistrationManager.RegistrationCallback mImsRegistrationCallback =
-            new RegistrationManager.RegistrationCallback() {
-        @Override
-        public void onRegistered(int imsTransportType) {
-            logi("onRegistered: type=" + imsTransportType);
-            mNetworkRegistrationType = imsTransportType;
-            mPresencePublication.onImsConnected();
-
-            // Also trigger PresencePublication#onFeatureCapabilityChanged method
-            MmTelFeature.MmTelCapabilities capabilities = null;
-            synchronized (mCapabilitiesLock) {
-                capabilities = mMmTelCapabilities;
-            }
-
-            if (capabilities != null) {
-                mPresencePublication.onFeatureCapabilityChanged(mNetworkRegistrationType,
-                        capabilities);
-            }
-        }
-
-        @Override
-        public void onUnregistered(ImsReasonInfo info) {
-            logi("onUnregistered");
-            mNetworkRegistrationType = AccessNetworkConstants.TRANSPORT_TYPE_INVALID;
-
-            // Also trigger PresencePublication#onFeatureCapabilityChanged method
-            MmTelFeature.MmTelCapabilities capabilities = null;
-            synchronized (mCapabilitiesLock) {
-                capabilities = mMmTelCapabilities;
-            }
-
-            if (capabilities != null) {
-                mPresencePublication.onFeatureCapabilityChanged(mNetworkRegistrationType,
-                        capabilities);
-            }
-            mPresencePublication.onImsDisconnected();
-        }
-    };
-
-    private ImsMmTelManager.CapabilityCallback mCapabilityCallback =
-            new ImsMmTelManager.CapabilityCallback() {
-        @Override
-        public void onCapabilitiesStatusChanged(MmTelFeature.MmTelCapabilities capabilities) {
-            if (capabilities == null) {
-                logw("onCapabilitiesStatusChanged: parameter is null");
-                return;
-            }
-            synchronized (mCapabilitiesLock) {
-                mMmTelCapabilities = capabilities;
-            }
-            mPresencePublication.onFeatureCapabilityChanged(mNetworkRegistrationType, capabilities);
-        }
-    };
-
-    private ProvisioningManager.Callback mProvisioningChangedCallback =
-            new ProvisioningManager.Callback() {
-        @Override
-        public void onProvisioningIntChanged(int item, int value) {
-            logi("onProvisioningIntChanged: item=" + item);
-            switch (item) {
-                case ProvisioningManager.KEY_EAB_PROVISIONING_STATUS:
-                case ProvisioningManager.KEY_VOLTE_PROVISIONING_STATUS:
-                case ProvisioningManager.KEY_VT_PROVISIONING_STATUS:
-                    mPresencePublication.handleProvisioningChanged();
-                    break;
-                default:
-                    break;
-            }
-        }
-    };
-
-    private boolean isCapabilityDiscoveryEnabled(int subId) {
-        try {
-            ProvisioningManager manager = ProvisioningManager.createForSubscriptionId(subId);
-            int discoveryEnabled = manager.getProvisioningIntValue(
-                    ProvisioningManager.KEY_RCS_CAPABILITY_DISCOVERY_ENABLED);
-            return (discoveryEnabled == ProvisioningManager.PROVISIONING_VALUE_ENABLED);
-        } catch (Exception e) {
-            logw("isCapabilityDiscoveryEnabled error: " + e.getMessage());
-        }
-        return false;
-    }
-
-    private boolean isEabProvisioned(Context context, int subId) {
-        if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
-            logw("isEabProvisioned error: invalid subscriptionId " + subId);
-            return false;
-        }
-
-        CarrierConfigManager configManager = (CarrierConfigManager)
-                context.getSystemService(Context.CARRIER_CONFIG_SERVICE);
-        if (configManager != null) {
-            PersistableBundle config = configManager.getConfigForSubId(subId);
-            if (config != null && !config.getBoolean(
-                    CarrierConfigManager.KEY_CARRIER_VOLTE_PROVISIONED_BOOL)) {
-                return true;
-            }
-        }
-
-        try {
-            ProvisioningManager manager = ProvisioningManager.createForSubscriptionId(subId);
-            int provisioningStatus = manager.getProvisioningIntValue(
-                    ProvisioningManager.KEY_EAB_PROVISIONING_STATUS);
-            return (provisioningStatus == ProvisioningManager.PROVISIONING_VALUE_ENABLED);
-        } catch (Exception e) {
-            logw("isEabProvisioned error: " + e.getMessage());
-        }
-        return false;
-    }
-
-    private ImsMmTelManager getImsMmTelManager(int subId) {
-        try {
-            ImsManager imsManager = (ImsManager) mContext.getSystemService(
-                    Context.TELEPHONY_IMS_SERVICE);
-            return (imsManager == null) ? null : imsManager.getImsMmTelManager(subId);
-        } catch (IllegalArgumentException e) {
-            logw("getImsMmTelManager error: " + e.getMessage());
-            return null;
-        }
-    }
-
-    private void logi(String log) {
-        Log.i(LOG_TAG, getLogPrefix().append(log).toString());
-    }
-
-    private void logw(String log) {
-        Log.w(LOG_TAG, getLogPrefix().append(log).toString());
-    }
-
-    private StringBuilder getLogPrefix() {
-        StringBuilder builder = new StringBuilder("[");
-        builder.append(mSlotId);
-        builder.append("->");
-        builder.append(mSubId);
-        builder.append("] ");
-        return builder;
-    }
-}
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/ImsConferenceTest.java b/tests/src/com/android/services/telephony/ImsConferenceTest.java
index f13d709..7e6488d 100644
--- a/tests/src/com/android/services/telephony/ImsConferenceTest.java
+++ b/tests/src/com/android/services/telephony/ImsConferenceTest.java
@@ -16,6 +16,8 @@
 
 package com.android.services.telephony;
 
+import static junit.framework.Assert.assertTrue;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -573,4 +575,40 @@
         assertEquals(0, imsConference.getNumberOfParticipants());
         verify(mConferenceHost.mMockCall).hangup();
     }
+
+    /**
+     * Verifies that an ImsConference can handle SIP and TEL URIs for both the P-Associated-Uri and
+     * conference event package identities.
+     */
+    @Test
+    public void testIsParticipantHost() {
+        // Simplest case, assume P-Associated-Uri is a tel URI and that the CEP participant is also
+        // a tel URI.
+        assertTrue(ImsConference.isParticipantHost(new Uri[] {
+                        Uri.parse("tel:+8616505551234")},
+                Uri.parse("tel:+8616505551234")));
+
+        // Assume P-Associated-Uri is a tel URI and the CEP participant is a sip URI.
+        assertTrue(ImsConference.isParticipantHost(new Uri[] {
+                        Uri.parse("tel:+8616505551234")},
+                Uri.parse("sip:+8616505551234@bj.ims.mnc011.mcc460.3gppnetwork.org")));
+
+        // Assume P-Associated-Uri is a sip URI and the CEP participant is a tel URI.
+        assertTrue(ImsConference.isParticipantHost(new Uri[] {
+                        Uri.parse("sip:+8616505551234@bj.ims.mnc011.mcc460.3gppnetwork.org")},
+                Uri.parse("tel:+8616505551234")));
+
+        // Assume both P-Associated-Uri and the CEP participant are SIP URIs.
+        assertTrue(ImsConference.isParticipantHost(new Uri[] {
+                        Uri.parse("sip:+8616505551234@bj.ims.mnc011.mcc460.3gppnetwork.org")},
+                Uri.parse("sip:+8616505551234@bj.ims.mnc011.mcc460.3gppnetwork.org")));
+
+        // Corner cases
+        assertFalse(ImsConference.isParticipantHost(new Uri[] {
+                        Uri.parse("tel:+8616505551234")}, Uri.fromParts("", "", "")));
+        assertFalse(ImsConference.isParticipantHost(new Uri[] {
+                        Uri.parse("tel:+8616505551234")}, null));
+        assertFalse(ImsConference.isParticipantHost(null, null));
+        assertFalse(ImsConference.isParticipantHost(new Uri[0], null));
+    }
 }
diff --git a/tests/src/com/android/services/telephony/TelephonyManagerTest.java b/tests/src/com/android/services/telephony/TelephonyManagerTest.java
new file mode 100644
index 0000000..e9cdc98
--- /dev/null
+++ b/tests/src/com/android/services/telephony/TelephonyManagerTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.telephony.emergency.EmergencyNumber;
+import android.test.mock.MockContext;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.telephony.ITelephony;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Unit tests for {@link TelephonyManager}. */
+@RunWith(AndroidJUnit4.class)
+public class TelephonyManagerTest {
+    private static final String PKG_NAME = "Unittest.TelephonyManagerTest";
+    private static final String TAG = "TelephonyManagerTest";
+
+    private ITelephony mMockITelephony;
+    private SubscriptionManager mMockSubscriptionManager;
+    private Context mMockContext;
+
+    private TelephonyManager mTelephonyManager;
+
+    private final MockContext mContext =
+            new MockContext() {
+                @Override
+                public String getOpPackageName() {
+                    return PKG_NAME;
+                }
+                @Override
+                public String getAttributionTag() {
+                    return TAG;
+                }
+                @Override
+                public Context getApplicationContext() {
+                    return null;
+                }
+                @Override
+                public Object getSystemService(String name) {
+                    switch (name) {
+                        case (Context.TELEPHONY_SUBSCRIPTION_SERVICE) : {
+                            return mMockSubscriptionManager;
+                        }
+                    }
+                    return null;
+                }
+            };
+
+    @Before
+    public void setUp() throws Exception {
+        mMockITelephony = mock(ITelephony.class);
+        mMockSubscriptionManager = mock(SubscriptionManager.class);
+        mMockContext = mock(Context.class);
+        when(mMockContext.getSystemService(eq(Context.TELEPHONY_SUBSCRIPTION_SERVICE)))
+                .thenReturn(mMockSubscriptionManager);
+
+        mTelephonyManager = new TelephonyManager(mContext);
+        TelephonyManager.setupITelephonyForTest(mMockITelephony);
+        TelephonyManager.enableServiceHandleCaching();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        TelephonyManager.setupITelephonyForTest(null);
+        TelephonyManager.disableServiceHandleCaching();
+    }
+
+    @Test
+    public void testGetEmergencyNumberListForCategories() throws Exception {
+        Map<Integer, List<EmergencyNumber>> emergencyNumberLists = new HashMap<>();
+        List<EmergencyNumber> emergencyNumberList = new ArrayList<>();
+        EmergencyNumber number_police = new EmergencyNumber(
+                "911",
+                "us",
+                "30",
+                EmergencyNumber.EMERGENCY_SERVICE_CATEGORY_POLICE,
+                new ArrayList<String>(),
+                EmergencyNumber.EMERGENCY_NUMBER_SOURCE_NETWORK_SIGNALING,
+                EmergencyNumber.EMERGENCY_CALL_ROUTING_NORMAL);
+        EmergencyNumber number_fire = new EmergencyNumber(
+                "912",
+                "us",
+                "30",
+                EmergencyNumber.EMERGENCY_SERVICE_CATEGORY_FIRE_BRIGADE,
+                new ArrayList<String>(),
+                EmergencyNumber.EMERGENCY_NUMBER_SOURCE_NETWORK_SIGNALING,
+                EmergencyNumber.EMERGENCY_CALL_ROUTING_NORMAL);
+        emergencyNumberList.add(number_police);
+        emergencyNumberList.add(number_fire);
+        final int test_sub_id = 1;
+        emergencyNumberLists.put(test_sub_id, emergencyNumberList);
+        when(mMockITelephony.getEmergencyNumberList(eq(PKG_NAME), eq(TAG))).thenReturn(
+                emergencyNumberLists);
+
+        // Call TelephonyManager.getEmergencyNumberList(Category)
+        Map<Integer, List<EmergencyNumber>> returnedEmergencyNumberLists =
+                mTelephonyManager.getEmergencyNumberList(
+                        EmergencyNumber.EMERGENCY_SERVICE_CATEGORY_POLICE);
+
+        // Verify the ITelephony service is called
+        verify(mMockITelephony, times(1)).getEmergencyNumberList(eq(PKG_NAME), eq(TAG));
+
+        // Verify the returned number list contains only the police number(s)
+        List<EmergencyNumber> returnedEmergencyNumberList = returnedEmergencyNumberLists.get(
+                test_sub_id);
+        for (EmergencyNumber num : returnedEmergencyNumberList) {
+            assertTrue(num.isInEmergencyServiceCategories(
+                    EmergencyNumber.EMERGENCY_SERVICE_CATEGORY_POLICE));
+        }
+    }
+}
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..5e05085
--- /dev/null
+++ b/tests/src/com/android/services/telephony/rcs/MessageTransportStateTrackerTest.java
@@ -0,0 +1,243 @@
+/*
+ * 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.anyLong;
+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(), anyLong());
+        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..fa439dc
--- /dev/null
+++ b/tests/src/com/android/services/telephony/rcs/SipDelegateBinderConnectionTest.java
@@ -0,0 +1,230 @@
+/*
+ * 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.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
+
+import android.os.IBinder;
+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 IBinder mTransportBinder;
+    @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();
+        doReturn(mTransportBinder).when(mMockTransport).asBinder();
+        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;
     }
 }
diff --git a/tests/src/com/android/services/telephony/rcs/TelephonyRcsServiceTest.java b/tests/src/com/android/services/telephony/rcs/TelephonyRcsServiceTest.java
index ffbb71d..7a9f9e3 100644
--- a/tests/src/com/android/services/telephony/rcs/TelephonyRcsServiceTest.java
+++ b/tests/src/com/android/services/telephony/rcs/TelephonyRcsServiceTest.java
@@ -50,8 +50,8 @@
 
     @Captor ArgumentCaptor<BroadcastReceiver> mReceiverCaptor;
     @Mock TelephonyRcsService.FeatureFactory mFeatureFactory;
-    @Mock UserCapabilityExchangeImpl mMockUceSlot0;
-    @Mock UserCapabilityExchangeImpl mMockUceSlot1;
+    @Mock UceControllerManager mMockUceSlot0;
+    @Mock UceControllerManager mMockUceSlot1;
     @Mock SipTransportController mMockSipTransportSlot0;
     @Mock SipTransportController mMockSipTransportSlot1;
     @Mock RcsFeatureController.RegistrationHelperFactory mRegistrationFactory;
@@ -70,9 +70,9 @@
         mFeatureControllerSlot1 = createFeatureController(1 /*slotId*/);
         doReturn(mFeatureControllerSlot0).when(mFeatureFactory).createController(any(), eq(0));
         doReturn(mFeatureControllerSlot1).when(mFeatureFactory).createController(any(), eq(1));
-        doReturn(mMockUceSlot0).when(mFeatureFactory).createUserCapabilityExchange(any(), eq(0),
+        doReturn(mMockUceSlot0).when(mFeatureFactory).createUceControllerManager(any(), eq(0),
                 anyInt());
-        doReturn(mMockUceSlot1).when(mFeatureFactory).createUserCapabilityExchange(any(), eq(1),
+        doReturn(mMockUceSlot1).when(mFeatureFactory).createUceControllerManager(any(), eq(1),
                 anyInt());
         doReturn(mMockSipTransportSlot0).when(mFeatureFactory).createSipTransportController(any(),
                 eq(0), anyInt());
@@ -89,20 +89,20 @@
     }
 
     @Test
-    public void testUserCapabilityExchangePresenceConnected() {
+    public void testUceControllerPresenceConnected() {
         setCarrierConfig(1 /*subId*/, CarrierConfigManager.KEY_USE_RCS_PRESENCE_BOOL,
                 true /*isEnabled*/);
         createRcsService(1 /*numSlots*/);
-        verify(mFeatureControllerSlot0).addFeature(mMockUceSlot0, UserCapabilityExchangeImpl.class);
+        verify(mFeatureControllerSlot0).addFeature(mMockUceSlot0, UceControllerManager.class);
         verify(mFeatureControllerSlot0).connect();
     }
 
     @Test
-    public void testUserCapabilityExchangeOptionsConnected() {
+    public void testUceControllerOptionsConnected() {
         setCarrierConfig(1 /*subId*/, CarrierConfigManager.KEY_USE_RCS_SIP_OPTIONS_BOOL,
                 true /*isEnabled*/);
         createRcsService(1 /*numSlots*/);
-        verify(mFeatureControllerSlot0).addFeature(mMockUceSlot0, UserCapabilityExchangeImpl.class);
+        verify(mFeatureControllerSlot0).addFeature(mMockUceSlot0, UceControllerManager.class);
         verify(mFeatureControllerSlot0).connect();
     }
 
@@ -111,7 +111,7 @@
         createRcsService(1 /*numSlots*/);
         // No carrier config set for UCE.
         verify(mFeatureControllerSlot0, never()).addFeature(mMockUceSlot0,
-                UserCapabilityExchangeImpl.class);
+                UceControllerManager.class);
         verify(mFeatureControllerSlot0, never()).connect();
     }
 
@@ -171,7 +171,7 @@
 
         sendCarrierConfigChanged(0, SubscriptionManager.INVALID_SUBSCRIPTION_ID);
         verify(mFeatureControllerSlot0, never()).addFeature(mMockUceSlot0,
-                UserCapabilityExchangeImpl.class);
+                UceControllerManager.class);
         verify(mFeatureControllerSlot0, never()).connect();
         verify(mFeatureControllerSlot0, never()).updateAssociatedSubscription(anyInt());
     }
@@ -184,25 +184,25 @@
         setCarrierConfig(2 /*subId*/, CarrierConfigManager.KEY_USE_RCS_PRESENCE_BOOL,
                 true /*isEnabled*/);
         TelephonyRcsService service = createRcsService(1 /*numSlots*/);
-        verify(mFeatureControllerSlot0).addFeature(mMockUceSlot0, UserCapabilityExchangeImpl.class);
+        verify(mFeatureControllerSlot0).addFeature(mMockUceSlot0, UceControllerManager.class);
         verify(mFeatureControllerSlot0).connect();
 
         // there should be no changes if the new num slots = old num
         service.updateFeatureControllerSize(1 /*newNumSlots*/);
         verify(mFeatureControllerSlot0, times(1)).addFeature(mMockUceSlot0,
-                UserCapabilityExchangeImpl.class);
+                UceControllerManager.class);
         verify(mFeatureControllerSlot0, times(1)).connect();
 
         // Add a new slot.
         verify(mFeatureControllerSlot1, never()).addFeature(mMockUceSlot1,
-                UserCapabilityExchangeImpl.class);
+                UceControllerManager.class);
         verify(mFeatureControllerSlot1, never()).connect();
         service.updateFeatureControllerSize(2 /*newNumSlots*/);
         // This shouldn't have changed for slot 0.
         verify(mFeatureControllerSlot0, times(1)).addFeature(mMockUceSlot0,
-                UserCapabilityExchangeImpl.class);
+                UceControllerManager.class);
         verify(mFeatureControllerSlot0, times(1)).connect();
-        verify(mFeatureControllerSlot1).addFeature(mMockUceSlot1, UserCapabilityExchangeImpl.class);
+        verify(mFeatureControllerSlot1).addFeature(mMockUceSlot1, UceControllerManager.class);
         verify(mFeatureControllerSlot1, times(1)).connect();
 
         // Remove a slot.
@@ -211,10 +211,10 @@
         service.updateFeatureControllerSize(1 /*newNumSlots*/);
         // addFeature/connect shouldn't have been called again
         verify(mFeatureControllerSlot0, times(1)).addFeature(mMockUceSlot0,
-                UserCapabilityExchangeImpl.class);
+                UceControllerManager.class);
         verify(mFeatureControllerSlot0, times(1)).connect();
         verify(mFeatureControllerSlot1, times(1)).addFeature(mMockUceSlot1,
-                UserCapabilityExchangeImpl.class);
+                UceControllerManager.class);
         verify(mFeatureControllerSlot1, times(1)).connect();
         // Verify destroy is only called for slot 1.
         verify(mFeatureControllerSlot0, never()).destroy();
@@ -228,8 +228,8 @@
         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);
+        verify(mFeatureControllerSlot0).addFeature(mMockUceSlot0, UceControllerManager.class);
+        verify(mFeatureControllerSlot1).addFeature(mMockUceSlot1, UceControllerManager.class);
         verify(mFeatureControllerSlot0).connect();
         verify(mFeatureControllerSlot1).connect();
 
@@ -248,7 +248,7 @@
         setCarrierConfig(1 /*subId*/, CarrierConfigManager.KEY_USE_RCS_PRESENCE_BOOL,
                 true /*isEnabled*/);
         createRcsService(1 /*numSlots*/);
-        verify(mFeatureControllerSlot0).addFeature(mMockUceSlot0, UserCapabilityExchangeImpl.class);
+        verify(mFeatureControllerSlot0).addFeature(mMockUceSlot0, UceControllerManager.class);
         verify(mFeatureControllerSlot0).connect();
 
 
@@ -256,7 +256,7 @@
         setCarrierConfig(1 /*subId*/, CarrierConfigManager.KEY_USE_RCS_PRESENCE_BOOL,
                 false /*isEnabled*/);
         sendCarrierConfigChanged(0 /*slotId*/, 1 /*subId*/);
-        verify(mFeatureControllerSlot0).removeFeature(UserCapabilityExchangeImpl.class);
+        verify(mFeatureControllerSlot0).removeFeature(UceControllerManager.class);
         verify(mFeatureControllerSlot0).updateAssociatedSubscription(1);
     }
 
@@ -284,7 +284,7 @@
     public void testCarrierConfigUpdateNoUceToUce() {
         createRcsService(1 /*numSlots*/);
         verify(mFeatureControllerSlot0, never()).addFeature(mMockUceSlot0,
-                UserCapabilityExchangeImpl.class);
+                UceControllerManager.class);
         verify(mFeatureControllerSlot0, never()).connect();
 
 
@@ -292,7 +292,7 @@
         setCarrierConfig(1 /*subId*/, CarrierConfigManager.KEY_USE_RCS_PRESENCE_BOOL,
                 true /*isEnabled*/);
         sendCarrierConfigChanged(0 /*slotId*/, 1 /*subId*/);
-        verify(mFeatureControllerSlot0).addFeature(mMockUceSlot0, UserCapabilityExchangeImpl.class);
+        verify(mFeatureControllerSlot0).addFeature(mMockUceSlot0, UceControllerManager.class);
         verify(mFeatureControllerSlot0).connect();
         verify(mFeatureControllerSlot0).updateAssociatedSubscription(1);
     }
diff --git a/tests/src/com/android/services/telephony/rcs/UceControllerManagerTest.java b/tests/src/com/android/services/telephony/rcs/UceControllerManagerTest.java
new file mode 100644
index 0000000..4148d13
--- /dev/null
+++ b/tests/src/com/android/services/telephony/rcs/UceControllerManagerTest.java
@@ -0,0 +1,245 @@
+/*
+ * 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.fail;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+
+import android.net.Uri;
+import android.telephony.ims.ImsException;
+import android.telephony.ims.aidl.IRcsUceControllerCallback;
+import android.telephony.ims.aidl.IRcsUcePublishStateCallback;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.TelephonyTestBase;
+import com.android.TestExecutorService;
+import com.android.ims.RcsFeatureManager;
+import com.android.ims.rcs.uce.UceController;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+
+@RunWith(AndroidJUnit4.class)
+public class UceControllerManagerTest extends TelephonyTestBase {
+
+    @Mock private UceController mUceController;
+    @Mock private RcsFeatureManager mRcsFeatureManager;
+
+    private final ExecutorService mExecutorService = new TestExecutorService();
+
+    private int mSlotId = 1;
+    private int mSubId = 1;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        doReturn(mSubId).when(mUceController).getSubId();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    public void testRcsConnected() throws Exception {
+        UceControllerManager controllerManager = getUceControllerManager();
+
+        controllerManager.onRcsConnected(mRcsFeatureManager);
+
+        verify(mUceController).onRcsConnected(mRcsFeatureManager);
+    }
+
+    @Test
+    public void testRcsDisconnected() throws Exception {
+        UceControllerManager uceCtrlManager = getUceControllerManager();
+
+        uceCtrlManager.onRcsDisconnected();
+
+        verify(mUceController).onRcsDisconnected();
+    }
+
+    @Test
+    public void testDestroy() throws Exception {
+        UceControllerManager uceCtrlManager = getUceControllerManager();
+
+        uceCtrlManager.onDestroy();
+
+        verify(mUceController).onDestroy();
+    }
+
+    @Test
+    public void testSubscriptionUpdated() throws Exception {
+        UceControllerManager uceCtrlManager = getUceControllerManager();
+
+        uceCtrlManager.onAssociatedSubscriptionUpdated(mSubId);
+
+        verify(mUceController).onDestroy();
+    }
+
+    @Test
+    public void testRequestCapabilitiesWithRcsUnavailable() throws Exception {
+        UceControllerManager uceCtrlManager = getUceControllerManager();
+        doReturn(true).when(mUceController).isUnavailable();
+        uceCtrlManager.onRcsDisconnected();
+
+        try {
+            List<Uri> contacts = Arrays.asList(Uri.fromParts("sip", "00000", null));
+            IRcsUceControllerCallback callback = Mockito.mock(IRcsUceControllerCallback.class);
+
+            uceCtrlManager.requestCapabilities(contacts, callback);
+
+            fail();
+        } catch (ImsException e) {
+            assertEquals(ImsException.CODE_ERROR_SERVICE_UNAVAILABLE, e.getCode());
+            return;
+        }
+        fail();
+    }
+
+    @Test
+    public void testRequestCapabilitiesWithRcsConnected() throws Exception {
+        UceControllerManager uceCtrlManager = getUceControllerManager();
+        doReturn(false).when(mUceController).isUnavailable();
+        uceCtrlManager.onRcsConnected(mRcsFeatureManager);
+
+        try {
+            List<Uri> contacts = Arrays.asList(Uri.fromParts("sip", "00000", null));
+            IRcsUceControllerCallback callback = Mockito.mock(IRcsUceControllerCallback.class);
+
+            uceCtrlManager.requestCapabilities(contacts, callback);
+
+            verify(mUceController).requestCapabilities(contacts, callback);
+        } catch (ImsException e) {
+            fail();
+        }
+    }
+
+    @Test
+    public void testRequestNetworkAvailability() throws Exception {
+        UceControllerManager uceCtrlManager = getUceControllerManager();
+        doReturn(false).when(mUceController).isUnavailable();
+        uceCtrlManager.onRcsConnected(mRcsFeatureManager);
+
+        Uri contact = Uri.fromParts("sip", "00000", null);
+        IRcsUceControllerCallback callback = Mockito.mock(IRcsUceControllerCallback.class);
+
+        uceCtrlManager.requestNetworkAvailability(contact, callback);
+
+        verify(mUceController).requestAvailability(contact, callback);
+    }
+
+    @Test
+    public void testRequestNetworkAvailabilityWithRcsUnavailable() throws Exception {
+        UceControllerManager uceCtrlManager = getUceControllerManager();
+        doReturn(true).when(mUceController).isUnavailable();
+        uceCtrlManager.onRcsDisconnected();
+
+        try {
+            Uri contact = Uri.fromParts("sip", "00000", null);
+            IRcsUceControllerCallback callback = Mockito.mock(IRcsUceControllerCallback.class);
+            uceCtrlManager.requestNetworkAvailability(contact, callback);
+            fail();
+        } catch (ImsException e) {
+            assertEquals(ImsException.CODE_ERROR_SERVICE_UNAVAILABLE, e.getCode());
+            return;
+        }
+        fail();
+    }
+
+    @Test
+    public void testGetPublishState() throws Exception {
+        UceControllerManager uceCtrlManager = getUceControllerManager();
+        doReturn(false).when(mUceController).isUnavailable();
+        uceCtrlManager.onRcsConnected(mRcsFeatureManager);
+
+        uceCtrlManager.getUcePublishState();
+
+        verify(mUceController).getUcePublishState();
+    }
+
+    @Test
+    public void testGetPublishStateWithRcsUnavailable() throws Exception {
+        UceControllerManager uceCtrlManager = getUceControllerManager();
+        doReturn(true).when(mUceController).isUnavailable();
+        uceCtrlManager.onRcsDisconnected();
+
+        try {
+            uceCtrlManager.getUcePublishState();
+            fail();
+        } catch (ImsException e) {
+            assertEquals(ImsException.CODE_ERROR_SERVICE_UNAVAILABLE, e.getCode());
+            return;
+        }
+        fail();
+    }
+
+    @Test
+    public void testRegisterPublishStateCallback() throws Exception {
+        UceControllerManager uceCtrlManager = getUceControllerManager();
+        IRcsUcePublishStateCallback callback = Mockito.mock(IRcsUcePublishStateCallback.class);
+
+        uceCtrlManager.registerPublishStateCallback(callback);
+
+        verify(mUceController).registerPublishStateCallback(callback);
+    }
+
+    @Test
+    public void testRegisterPublishStateCallbackWithRcsUnavailable() throws Exception {
+        UceControllerManager uceCtrlManager = getUceControllerManager();
+        doReturn(true).when(mUceController).isUnavailable();
+        uceCtrlManager.onRcsDisconnected();
+
+        try {
+            IRcsUcePublishStateCallback callback = Mockito.mock(IRcsUcePublishStateCallback.class);
+            uceCtrlManager.registerPublishStateCallback(callback);
+            fail();
+        } catch (ImsException e) {
+            assertEquals(ImsException.CODE_ERROR_SERVICE_UNAVAILABLE, e.getCode());
+            return;
+        }
+        fail();
+    }
+
+    @Test
+    public void testUnregisterPublishStateCallback() throws Exception {
+        UceControllerManager uceCtrlManager = getUceControllerManager();
+        IRcsUcePublishStateCallback callback = Mockito.mock(IRcsUcePublishStateCallback.class);
+
+        uceCtrlManager.unregisterPublishStateCallback(callback);
+
+        verify(mUceController).unregisterPublishStateCallback(callback);
+    }
+
+    private UceControllerManager getUceControllerManager() {
+        UceControllerManager manager = new UceControllerManager(mContext, mSlotId, mSubId,
+                mExecutorService);
+        manager.setUceController(mUceController);
+        return manager;
+    }
+}
diff --git a/tests/src/com/android/services/telephony/rcs/UserCapabilityExchangeImplTest.java b/tests/src/com/android/services/telephony/rcs/UserCapabilityExchangeImplTest.java
deleted file mode 100644
index 3cbe8bf..0000000
--- a/tests/src/com/android/services/telephony/rcs/UserCapabilityExchangeImplTest.java
+++ /dev/null
@@ -1,307 +0,0 @@
-/*
- * 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.anyList;
-import static org.mockito.Mockito.atLeast;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.net.Uri;
-import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.RemoteCallbackList;
-import android.telephony.ims.ImsManager;
-import android.telephony.ims.ImsMmTelManager;
-import android.telephony.ims.RcsContactPresenceTuple;
-import android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities;
-import android.telephony.ims.RcsContactUceCapability;
-import android.telephony.ims.RcsContactUceCapability.PresenceBuilder;
-import android.telephony.ims.RegistrationManager;
-import android.telephony.ims.aidl.IRcsUceControllerCallback;
-import android.telephony.ims.aidl.IRcsUcePublishStateCallback;
-import android.telephony.ims.stub.RcsCapabilityExchange;
-import android.telephony.ims.stub.RcsPresenceExchangeImplBase;
-
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.TelephonyTestBase;
-import com.android.ims.RcsFeatureManager;
-import com.android.ims.RcsFeatureManager.RcsFeatureCallbacks;
-import com.android.ims.ResultCode;
-import com.android.service.ims.presence.PresenceBase;
-import com.android.service.ims.presence.PresencePublication;
-import com.android.service.ims.presence.PresencePublisher;
-import com.android.service.ims.presence.PresenceSubscriber;
-import com.android.service.ims.presence.SubscribePublisher;
-
-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 org.mockito.Mockito;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.Executor;
-
-@RunWith(AndroidJUnit4.class)
-public class UserCapabilityExchangeImplTest extends TelephonyTestBase {
-
-    private int  mSlotId = 0;
-    private int mSubId = 1;
-    private int mUpdatedSubId = 2;
-
-    @Captor ArgumentCaptor<IRcsUcePublishStateCallback> mPublishStateCallbacksCaptor;
-
-    @Mock PresencePublication mPresencePublication;
-    @Mock PresenceSubscriber mPresenceSubscriber;
-    @Mock RcsFeatureManager mRcsFeatureManager;
-    @Mock ImsMmTelManager mImsMmTelManager;
-    @Mock RemoteCallbackList<IRcsUcePublishStateCallback> mPublishStateCallbacks;
-
-    private Looper mLooper;
-
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-
-        ImsManager imsManager =
-                (ImsManager) mContext.getSystemService(Context.TELEPHONY_IMS_SERVICE);
-        when(imsManager.getImsMmTelManager(mSubId)).thenReturn(mImsMmTelManager);
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        super.tearDown();
-
-        if (mLooper != null) {
-            mLooper.quit();
-            mLooper = null;
-        }
-    }
-
-    @Test
-    public void testServiceConnected() throws Exception {
-        UserCapabilityExchangeImpl uceImpl = createUserCapabilityExchangeImpl();
-        uceImpl.onRcsConnected(mRcsFeatureManager);
-
-        verify(mRcsFeatureManager).addFeatureListenerCallback(any(RcsFeatureCallbacks.class));
-        verify(mPresencePublication).updatePresencePublisher(any(PresencePublisher.class));
-        verify(mPresenceSubscriber).updatePresenceSubscriber(any(SubscribePublisher.class));
-    }
-
-    @Test
-    public void testServiceDisconnected() throws Exception {
-        UserCapabilityExchangeImpl uceImpl = createUserCapabilityExchangeImpl();
-        uceImpl.onRcsDisconnected();
-
-        verify(mPresencePublication).removePresencePublisher();
-        verify(mPresenceSubscriber).removePresenceSubscriber();
-    }
-
-    @Test
-    public void testSubscriptionUpdated() throws Exception {
-        UserCapabilityExchangeImpl uceImpl = createUserCapabilityExchangeImpl();
-        uceImpl.onAssociatedSubscriptionUpdated(mUpdatedSubId);
-
-        verify(mImsMmTelManager).registerImsRegistrationCallback(any(Executor.class),
-                any(RegistrationManager.RegistrationCallback.class));
-        verify(mImsMmTelManager).registerMmTelCapabilityCallback(any(Executor.class),
-                any(ImsMmTelManager.CapabilityCallback.class));
-        verify(mPresencePublication).handleAssociatedSubscriptionChanged(mUpdatedSubId);
-        verify(mPresenceSubscriber).handleAssociatedSubscriptionChanged(mUpdatedSubId);
-    }
-
-    @Test
-    public void testUcePublishStateRetrieval() throws Exception {
-        UserCapabilityExchangeImpl uceImpl = createUserCapabilityExchangeImpl();
-        uceImpl.getUcePublishState();
-
-        verify(mPresencePublication).getPublishState();
-    }
-
-    @Test
-    public void testRegisterPublishStateCallbacks() throws Exception {
-        UserCapabilityExchangeImpl uceImpl = createUserCapabilityExchangeImpl();
-        uceImpl.registerPublishStateCallback(any(IRcsUcePublishStateCallback.class));
-        verify(mPublishStateCallbacks).register(mPublishStateCallbacksCaptor.capture());
-    }
-
-    @Test
-    public void testOnNotifyUpdateCapabilities() throws Exception {
-        UserCapabilityExchangeImpl uceImpl = createUserCapabilityExchangeImpl();
-        uceImpl.onRcsConnected(mRcsFeatureManager);
-
-        int triggerType = RcsPresenceExchangeImplBase.CAPABILITY_UPDATE_TRIGGER_MOVE_TO_IWLAN;
-        uceImpl.mRcsFeatureCallback.onNotifyUpdateCapabilities(triggerType);
-        waitForMs(1000);
-
-        verify(mPresencePublication).onStackPublishRequested(triggerType);
-    }
-
-    @Test
-    public void testRequestPublicationWithSuccessfulResponse() throws Exception {
-        int taskId = 1;
-        int sipResponse = 200;
-        Uri contact = Uri.fromParts("sip", "test", null);
-        RcsContactUceCapability capability = getRcsContactUceCapability(contact);
-
-        UserCapabilityExchangeImpl uceImpl = createUserCapabilityExchangeImpl();
-        uceImpl.onRcsConnected(mRcsFeatureManager);
-
-        doAnswer(invocation -> {
-            uceImpl.mRcsFeatureCallback.onCommandUpdate(RcsCapabilityExchange.COMMAND_CODE_SUCCESS,
-                    taskId);
-            uceImpl.mRcsFeatureCallback.onNetworkResponse(sipResponse, null, taskId);
-            return null;
-        }).when(mRcsFeatureManager).requestPublication(capability, taskId);
-
-        // Request publication
-        int result = uceImpl.requestPublication(capability, contact.toString(), taskId);
-
-        assertEquals(ResultCode.SUCCESS, result);
-        verify(mPresencePublication).onCommandStatusUpdated(taskId, taskId, ResultCode.SUCCESS);
-        verify(mPresencePublication).onSipResponse(taskId, sipResponse, null);
-    }
-
-    @Test
-    public void testRequestPublicationWithFailedResponse() throws Exception {
-        int taskId = 1;
-        Uri contact = Uri.fromParts("sip", "test", null);
-        RcsContactUceCapability capability = getRcsContactUceCapability(contact);
-
-        UserCapabilityExchangeImpl uceImpl = createUserCapabilityExchangeImpl();
-        uceImpl.onRcsConnected(mRcsFeatureManager);
-
-        doAnswer(invocation -> {
-            uceImpl.mRcsFeatureCallback.onCommandUpdate(
-                    RcsCapabilityExchange.COMMAND_CODE_GENERIC_FAILURE, taskId);
-            return null;
-        }).when(mRcsFeatureManager).requestPublication(capability, taskId);
-
-        // Request publication
-        int result = uceImpl.requestPublication(capability, contact.toString(), taskId);
-
-        assertEquals(ResultCode.SUCCESS, result);
-        verify(mPresencePublication).onCommandStatusUpdated(taskId, taskId,
-                ResultCode.PUBLISH_GENERIC_FAILURE);
-    }
-
-    private RcsContactUceCapability getRcsContactUceCapability(Uri contact) {
-        ServiceCapabilities.Builder servCapsBuilder = new ServiceCapabilities.Builder(true, true);
-        servCapsBuilder.addSupportedDuplexMode(ServiceCapabilities.DUPLEX_MODE_FULL);
-
-        RcsContactPresenceTuple.Builder tupleBuilder = new RcsContactPresenceTuple.Builder(
-                RcsContactPresenceTuple.TUPLE_BASIC_STATUS_OPEN,
-                RcsContactPresenceTuple.SERVICE_ID_MMTEL, "1.0");
-        tupleBuilder.addContactUri(contact).addServiceCapabilities(servCapsBuilder.build());
-
-        PresenceBuilder presenceBuilder = new PresenceBuilder(contact,
-                RcsContactUceCapability.SOURCE_TYPE_CACHED,
-                RcsContactUceCapability.REQUEST_RESULT_FOUND);
-        presenceBuilder.addCapabilityTuple(tupleBuilder.build());
-        return presenceBuilder.build();
-    }
-
-    @Test
-    public void testRequestCapability() throws Exception {
-        int taskId = 1;
-        int sipResponse = 200;
-        List<RcsContactUceCapability> infos = new ArrayList<>();
-        List<Uri> contacts = Arrays.asList(Uri.fromParts("sip", "00000", null));
-        IRcsUceControllerCallback callback = Mockito.mock(IRcsUceControllerCallback.class);
-
-        UserCapabilityExchangeImpl uceImpl = createUserCapabilityExchangeImpl();
-        uceImpl.onRcsConnected(mRcsFeatureManager);
-
-        when(mPresenceSubscriber.requestCapability(anyList(), any())).thenReturn(taskId);
-
-        doAnswer(invocation -> {
-            uceImpl.mRcsFeatureCallback.onCommandUpdate(RcsCapabilityExchange.COMMAND_CODE_SUCCESS,
-                    taskId);
-            uceImpl.mRcsFeatureCallback.onNetworkResponse(sipResponse, null, taskId);
-            uceImpl.mRcsFeatureCallback.onCapabilityRequestResponsePresence(infos, taskId);
-            return null;
-        }).when(mRcsFeatureManager).requestCapabilities(anyList(), anyInt());
-
-        uceImpl.requestCapabilities(contacts, callback);
-        uceImpl.requestCapability(new String[] {"00000"}, taskId);
-
-        verify(mPresenceSubscriber).onCommandStatusUpdated(taskId, taskId, ResultCode.SUCCESS);
-        verify(mPresenceSubscriber).onSipResponse(taskId, sipResponse, null);
-        verify(mPresenceSubscriber).updatePresences(taskId, infos, true, null);
-    }
-
-    @Test
-    public void testUpdatePublisherState() throws Exception {
-        IRcsUcePublishStateCallback callback = Mockito.mock(IRcsUcePublishStateCallback.class);
-        doAnswer(invocation -> {
-            callback.onPublishStateChanged(anyInt());
-            return null;
-        }).when(mPublishStateCallbacks).broadcast(any());
-
-        UserCapabilityExchangeImpl uceImpl = createUserCapabilityExchangeImpl();
-        uceImpl.onRcsConnected(mRcsFeatureManager);
-        uceImpl.registerPublishStateCallback(callback);
-        uceImpl.updatePublisherState(PresenceBase.PUBLISH_STATE_200_OK);
-
-        assertEquals(PresenceBase.PUBLISH_STATE_200_OK, uceImpl.getPublisherState());
-        verify(callback).onPublishStateChanged(anyInt());
-    }
-
-    @Test
-    public void testUnpublish() throws Exception {
-        IRcsUcePublishStateCallback callback = Mockito.mock(IRcsUcePublishStateCallback.class);
-        doAnswer(invocation -> {
-            callback.onPublishStateChanged(anyInt());
-            return null;
-        }).when(mPublishStateCallbacks).broadcast(any());
-
-        UserCapabilityExchangeImpl uceImpl = createUserCapabilityExchangeImpl();
-        uceImpl.onRcsConnected(mRcsFeatureManager);
-        uceImpl.mRcsFeatureCallback.onUnpublish();
-        waitForMs(1000);
-
-        verify(mPresencePublication).setPublishState(PresenceBase.PUBLISH_STATE_NOT_PUBLISHED);
-    }
-
-    private UserCapabilityExchangeImpl createUserCapabilityExchangeImpl() throws Exception {
-        HandlerThread handlerThread = new HandlerThread("UceImplHandlerThread");
-        handlerThread.start();
-        mLooper = handlerThread.getLooper();
-        UserCapabilityExchangeImpl uceImpl = new UserCapabilityExchangeImpl(mContext, mSlotId,
-                mSubId, mLooper, mPresencePublication, mPresenceSubscriber,
-                mPublishStateCallbacks);
-        verify(mPresencePublication).handleAssociatedSubscriptionChanged(1);
-        verify(mPresenceSubscriber).handleAssociatedSubscriptionChanged(1);
-        waitForHandlerAction(uceImpl.getHandler(), 1000);
-        verify(mImsMmTelManager, atLeast(1)).registerImsRegistrationCallback(
-                any(Executor.class), any(RegistrationManager.RegistrationCallback.class));
-        verify(mContext).registerReceiver(any(), any());
-        return uceImpl;
-    }
-}