Merge "RCS Provisioning APIs for Single Registration"
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index f47a6b0..37e009d 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -59,6 +59,7 @@
     <protected-broadcast android:name= "com.android.internal.telephony.CARRIER_SIGNAL_PCO_VALUE" />
     <protected-broadcast android:name= "com.android.internal.telephony.CARRIER_SIGNAL_RESET" />
     <protected-broadcast android:name= "com.android.internal.telephony.CARRIER_SIGNAL_DEFAULT_NETWORK_AVAILABLE" />
+    <protected-broadcast android:name= "com.android.internal.telephony.PROVISION" />
     <protected-broadcast android:name= "com.android.internal.telephony.ACTION_LINE1_NUMBER_ERROR_DETECTED" />
     <protected-broadcast android:name= "com.android.internal.provider.action.VOICEMAIL_SMS_RECEIVED" />
     <protected-broadcast android:name= "com.android.intent.isim_refresh" />
@@ -88,6 +89,7 @@
     <protected-broadcast android:name= "android.telephony.action.SIM_SLOT_STATUS_CHANGED" />
     <protected-broadcast android:name= "android.telephony.action.SUBSCRIPTION_CARRIER_IDENTITY_CHANGED" />
     <protected-broadcast android:name= "android.telephony.action.SUBSCRIPTION_SPECIFIC_CARRIER_IDENTITY_CHANGED" />
+    <protected-broadcast android:name= "android.telephony.action.TOGGLE_PROVISION" />
     <protected-broadcast android:name= "android.telephony.action.NETWORK_COUNTRY_CHANGED" />
     <protected-broadcast android:name= "android.telephony.action.PRIMARY_SUBSCRIPTION_LIST_CHANGED" />
     <protected-broadcast android:name= "android.telephony.action.MULTI_SIM_CONFIG_CHANGED" />
diff --git a/src/com/android/phone/ImsRcsController.java b/src/com/android/phone/ImsRcsController.java
index 52069b8..12c8cda 100644
--- a/src/com/android/phone/ImsRcsController.java
+++ b/src/com/android/phone/ImsRcsController.java
@@ -466,6 +466,24 @@
         }
     }
 
+    @Override
+    public void triggerNetworkRegistration(int subId, ISipDelegate connection, int sipCode,
+            String sipReason) {
+        enforceModifyPermission();
+
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            SipTransportController transport = getRcsFeatureController(subId).getFeature(
+                    SipTransportController.class);
+            if (transport == null) {
+                return;
+            }
+            transport.triggerFullNetworkRegistration(subId, connection, sipCode, sipReason);
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
     /**
      * Registers for updates to the RcsFeature connection through the IImsServiceFeatureCallback
      * callback.
diff --git a/src/com/android/phone/PhoneInterfaceManager.java b/src/com/android/phone/PhoneInterfaceManager.java
index 796efd0..9b2cac7 100755
--- a/src/com/android/phone/PhoneInterfaceManager.java
+++ b/src/com/android/phone/PhoneInterfaceManager.java
@@ -31,7 +31,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
-import android.content.pm.ApplicationInfo;
 import android.content.pm.ComponentInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
@@ -2260,7 +2259,7 @@
         mApp.getSystemService(AppOpsManager.class)
                 .checkPackage(Binder.getCallingUid(), callingPackage);
 
-        final int targetSdk = getTargetSdk(callingPackage);
+        final int targetSdk = TelephonyPermissions.getTargetSdk(mApp, callingPackage);
         if (targetSdk > android.os.Build.VERSION_CODES.R) {
             // Callers targeting S have no business invoking this method.
             return;
@@ -2695,30 +2694,11 @@
                 android.Manifest.permission.CONTROL_LOCATION_UPDATES, null);
     }
 
-    /**
-     * Returns the target SDK version number for a given package name.
-     *
-     * This call MUST be invoked before clearing the calling UID.
-     *
-     * @return target SDK if the package is found or INT_MAX.
-     */
-    private int getTargetSdk(String packageName) {
-        try {
-            final ApplicationInfo ai = mApp.getPackageManager().getApplicationInfoAsUser(
-                    packageName, 0, UserHandle.getUserHandleForUid(Binder.getCallingUid()));
-            if (ai != null) return ai.targetSdkVersion;
-        } catch (PackageManager.NameNotFoundException unexpected) {
-            loge("Failed to get package info for pkg="
-                    + packageName + ", uid=" + Binder.getCallingUid());
-        }
-        return Integer.MAX_VALUE;
-    }
-
     @Override
     @SuppressWarnings("unchecked")
     public List<NeighboringCellInfo> getNeighboringCellInfo(String callingPackage,
             String callingFeatureId) {
-        final int targetSdk = getTargetSdk(callingPackage);
+        final int targetSdk = TelephonyPermissions.getTargetSdk(mApp, callingPackage);
         if (targetSdk >= android.os.Build.VERSION_CODES.Q) {
             throw new SecurityException(
                     "getNeighboringCellInfo() is unavailable to callers targeting Q+ SDK levels.");
@@ -2777,7 +2757,7 @@
                 return new ArrayList<>();
         }
 
-        final int targetSdk = getTargetSdk(callingPackage);
+        final int targetSdk = TelephonyPermissions.getTargetSdk(mApp, callingPackage);
         if (targetSdk >= android.os.Build.VERSION_CODES.Q) {
             return getCachedCellInfo();
         }
@@ -2830,13 +2810,15 @@
                                 .build());
         switch (locationResult) {
             case DENIED_HARD:
-                if (getTargetSdk(callingPackage) < Build.VERSION_CODES.Q) {
+                if (TelephonyPermissions
+                        .getTargetSdk(mApp, callingPackage) < Build.VERSION_CODES.Q) {
                     // Safetynet logging for b/154934934
                     EventLog.writeEvent(0x534e4554, "154934934", Binder.getCallingUid());
                 }
                 throw new SecurityException("Not allowed to access cell info");
             case DENIED_SOFT:
-                if (getTargetSdk(callingPackage) < Build.VERSION_CODES.Q) {
+                if (TelephonyPermissions
+                        .getTargetSdk(mApp, callingPackage) < Build.VERSION_CODES.Q) {
                     // Safetynet logging for b/154934934
                     EventLog.writeEvent(0x534e4554, "154934934", Binder.getCallingUid());
                 }
@@ -4561,7 +4543,7 @@
     @Override
     public int getNetworkTypeForSubscriber(int subId, String callingPackage,
             String callingFeatureId) {
-        final int targetSdk = getTargetSdk(callingPackage);
+        final int targetSdk = TelephonyPermissions.getTargetSdk(mApp, callingPackage);
         if (targetSdk > android.os.Build.VERSION_CODES.Q) {
             return getDataNetworkTypeForSubscriber(subId, callingPackage, callingFeatureId);
         } else if (targetSdk == android.os.Build.VERSION_CODES.Q
diff --git a/src/com/android/services/telephony/rcs/DelegateBinderStateManager.java b/src/com/android/services/telephony/rcs/DelegateBinderStateManager.java
index 39e9965..9d2c5d6 100644
--- a/src/com/android/services/telephony/rcs/DelegateBinderStateManager.java
+++ b/src/com/android/services/telephony/rcs/DelegateBinderStateManager.java
@@ -19,10 +19,11 @@
 import android.telephony.ims.DelegateRegistrationState;
 import android.telephony.ims.DelegateRequest;
 import android.telephony.ims.FeatureTagState;
+import android.telephony.ims.SipDelegateConnection;
 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.ISipTransport;
 
 import com.android.internal.annotations.VisibleForTesting;
 
@@ -61,8 +62,8 @@
          * denied. See {@link SipDelegateBinderConnectionStub} and
          * {@link SipDelegateBinderConnection}
          */
-        DelegateBinderStateManager create(int subId, ISipTransport sipTransport,
-                DelegateRequest requestedConfig, Set<FeatureTagState> transportDeniedTags,
+        DelegateBinderStateManager create(int subId, DelegateRequest requestedConfig,
+                Set<FeatureTagState> transportDeniedTags,
                 Executor executor, List<StateCallback> stateCallbacks);
     }
 
@@ -89,4 +90,11 @@
      *         Contains the reason the SipDelegate reported it was destroyed.
      */
     void destroy(int reason, Consumer<Integer> destroyedConsumer);
+
+    /**
+     * Called by IMS application, see
+     * {@link SipDelegateManager#triggerFullNetworkRegistration(SipDelegateConnection, int, String)}
+     * for more information about when this is called.
+     */
+    void triggerFullNetworkRegistration(int sipCode, String sipReason);
 }
diff --git a/src/com/android/services/telephony/rcs/SipDelegateBinderConnection.java b/src/com/android/services/telephony/rcs/SipDelegateBinderConnection.java
index 1a77f2b..3f6f269 100644
--- a/src/com/android/services/telephony/rcs/SipDelegateBinderConnection.java
+++ b/src/com/android/services/telephony/rcs/SipDelegateBinderConnection.java
@@ -24,6 +24,7 @@
 import android.telephony.ims.FeatureTagState;
 import android.telephony.ims.SipDelegateImsConfiguration;
 import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.aidl.IImsRegistration;
 import android.telephony.ims.aidl.ISipDelegate;
 import android.telephony.ims.aidl.ISipDelegateMessageCallback;
 import android.telephony.ims.aidl.ISipDelegateStateCallback;
@@ -118,6 +119,7 @@
             };
 
     private final ISipTransport mSipTransport;
+    private final IImsRegistration mImsRegistration;
     private final DelegateRequest mRequestedConfig;
 
     private ISipDelegate mDelegateBinder;
@@ -129,6 +131,8 @@
      * {@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 registrationImpl The ImsRegistration implementation that will be used to manage
+     *                         registration changes in relation to the 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.
@@ -138,10 +142,12 @@
      *                       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) {
+            IImsRegistration registrationImpl, DelegateRequest requestedConfig,
+            Set<FeatureTagState> transportDeniedTags, Executor executor,
+            List<StateCallback> stateCallbacks) {
         mSubId = subId;
         mSipTransport = sipTransport;
+        mImsRegistration = registrationImpl;
         mRequestedConfig = requestedConfig;
         mDeniedTags = transportDeniedTags;
         mExecutor = executor;
@@ -184,6 +190,15 @@
         }
     }
 
+    @Override
+    public void triggerFullNetworkRegistration(int sipCode, String sipReason) {
+        try {
+            mImsRegistration.triggerFullNetworkRegistration(sipCode, sipReason);
+        } catch (RemoteException e) {
+            logw("triggerFullNetworkRegistration called on unreachable ImsRegistration:" + e);
+        }
+    }
+
     private void notifySipDelegateCreated(ISipDelegate delegate,
             List<FeatureTagState> deniedFeatureTags) {
         logi("Delegate Created: " + delegate + ", deniedTags:" + deniedFeatureTags);
diff --git a/src/com/android/services/telephony/rcs/SipDelegateBinderConnectionStub.java b/src/com/android/services/telephony/rcs/SipDelegateBinderConnectionStub.java
index 888af94..ef12eb8 100644
--- a/src/com/android/services/telephony/rcs/SipDelegateBinderConnectionStub.java
+++ b/src/com/android/services/telephony/rcs/SipDelegateBinderConnectionStub.java
@@ -77,4 +77,10 @@
             destroyedConsumer.accept(reason);
         });
     }
+
+    @Override
+    public void triggerFullNetworkRegistration(int sipCode, String sipReason) {
+        // This stub is not connected to an ImsService, so this method is intentionally not
+        // implemented.
+    }
 }
diff --git a/src/com/android/services/telephony/rcs/SipDelegateController.java b/src/com/android/services/telephony/rcs/SipDelegateController.java
index ed50778..4b3176a 100644
--- a/src/com/android/services/telephony/rcs/SipDelegateController.java
+++ b/src/com/android/services/telephony/rcs/SipDelegateController.java
@@ -21,6 +21,7 @@
 import android.telephony.ims.FeatureTagState;
 import android.telephony.ims.SipDelegateConnection;
 import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.aidl.IImsRegistration;
 import android.telephony.ims.aidl.ISipDelegate;
 import android.telephony.ims.aidl.ISipDelegateConnectionStateCallback;
 import android.telephony.ims.aidl.ISipDelegateMessageCallback;
@@ -49,10 +50,18 @@
 public class SipDelegateController {
     static final String LOG_TAG = "SipDelegateC";
 
-    private DelegateBinderStateManager.Factory mBinderConnectionFactory =
-            new DelegateBinderStateManager.Factory() {
+    private class BinderConnectionFactory implements DelegateBinderStateManager.Factory {
+
+        private final ISipTransport mSipTransportImpl;
+        private final IImsRegistration mImsRegistrationImpl;
+
+        BinderConnectionFactory(ISipTransport transport, IImsRegistration registration) {
+            mSipTransportImpl = transport;
+            mImsRegistrationImpl = registration;
+        }
+
         @Override
-        public DelegateBinderStateManager create(int subId, ISipTransport sipTransport,
+        public DelegateBinderStateManager create(int subId,
                 DelegateRequest requestedConfig, Set<FeatureTagState> transportDeniedTags,
                 Executor executor, List<DelegateBinderStateManager.StateCallback> stateCallbacks) {
             // We should not actually create a SipDelegate in this case.
@@ -60,32 +69,33 @@
                 return new SipDelegateBinderConnectionStub(transportDeniedTags, executor,
                         stateCallbacks);
             }
-            return new SipDelegateBinderConnection(mSubId, mSipTransportImpl, requestedConfig,
-                    transportDeniedTags, mExecutorService, stateCallbacks);
+            return new SipDelegateBinderConnection(mSubId, mSipTransportImpl, mImsRegistrationImpl,
+                    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 DelegateBinderStateManager.Factory mBinderConnectionFactory;
     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,
+            ISipTransport transportImpl, IImsRegistration registrationImpl,
+            ScheduledExecutorService executorService,
             ISipDelegateConnectionStateCallback stateCallback,
             ISipDelegateMessageCallback messageCallback) {
         mSubId = subId;
         mPackageName = packageName;
         mInitialRequest = initialRequest;
-        mSipTransportImpl = sipTransportImpl;
         mExecutorService = executorService;
+        mBinderConnectionFactory = new BinderConnectionFactory(transportImpl, registrationImpl);
 
         mMessageTransportStateTracker = new MessageTransportStateTracker(mSubId, executorService,
                 messageCallback);
@@ -98,14 +108,13 @@
      */
     @VisibleForTesting
     public SipDelegateController(int subId, DelegateRequest initialRequest, String packageName,
-            ISipTransport sipTransportImpl, ScheduledExecutorService executorService,
+            ScheduledExecutorService executorService,
             MessageTransportStateTracker messageTransportStateTracker,
             DelegateStateTracker delegateStateTracker,
             DelegateBinderStateManager.Factory connectionFactory) {
         mSubId = subId;
         mInitialRequest = initialRequest;
         mPackageName = packageName;
-        mSipTransportImpl = sipTransportImpl;
         mExecutorService = executorService;
         mMessageTransportStateTracker = messageTransportStateTracker;
         mDelegateStateTracker = delegateStateTracker;
@@ -249,6 +258,21 @@
         }, mExecutorService);
     };
 
+    /**
+     * The IMS application is notifying the ImsService that it has received a response to a request
+     * that will require that the IMS registration be torn down and brought back up.
+     *<p>
+     * See {@link SipDelegateManager#triggerFullNetworkRegistration} for more information.
+     */
+    public void triggerFullNetworkRegistration(int sipCode, String sipReason) {
+        logi("triggerFullNetworkRegistration, code=" + sipCode + ", reason=" + sipReason);
+        if (mBinderConnection != null) {
+            mBinderConnection.triggerFullNetworkRegistration(sipCode, sipReason);
+        } else {
+            logw("triggerFullNetworkRegistration called when binder connection is null");
+        }
+    }
+
     private static int getMessageFailReasonFromDestroyReason(int destroyReason) {
         switch (destroyReason) {
             case SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SERVICE_DEAD:
@@ -340,7 +364,7 @@
         stateCallbacks.add(mDelegateStateTracker);
         stateCallbacks.add(mMessageTransportStateTracker);
 
-        return mBinderConnectionFactory.create(mSubId, mSipTransportImpl,
+        return mBinderConnectionFactory.create(mSubId,
                 new DelegateRequest(supportedSet), deniedSet, mExecutorService, stateCallbacks);
     }
 
diff --git a/src/com/android/services/telephony/rcs/SipTransportController.java b/src/com/android/services/telephony/rcs/SipTransportController.java
index 5d817ba..028e49f 100644
--- a/src/com/android/services/telephony/rcs/SipTransportController.java
+++ b/src/com/android/services/telephony/rcs/SipTransportController.java
@@ -19,12 +19,14 @@
 import android.app.role.OnRoleHoldersChangedListener;
 import android.app.role.RoleManager;
 import android.content.Context;
+import android.os.RemoteException;
 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.IImsRegistration;
 import android.telephony.ims.aidl.ISipDelegate;
 import android.telephony.ims.aidl.ISipDelegateConnectionStateCallback;
 import android.telephony.ims.aidl.ISipDelegateMessageCallback;
@@ -206,7 +208,8 @@
     public interface SipDelegateControllerFactory {
         /** See {@link SipDelegateController} */
         SipDelegateController create(int subId, DelegateRequest initialRequest, String packageName,
-                ISipTransport sipTransportImpl, ScheduledExecutorService executorService,
+                ISipTransport sipTransportImpl,  IImsRegistration registrationImpl,
+                ScheduledExecutorService executorService,
                 ISipDelegateConnectionStateCallback stateCallback,
                 ISipDelegateMessageCallback messageCallback);
     }
@@ -348,6 +351,16 @@
     }
 
     /**
+     * The remote IMS application has requested that the ImsService tear down and re-register for
+     * IMS features due to an error it received on the network in response to a SIP request.
+     */
+    public void triggerFullNetworkRegistration(int subId, ISipDelegate connection, int sipCode,
+            String sipReason) {
+        mExecutorService.execute(() -> triggerFullNetworkRegistrationInternal(subId, connection,
+                sipCode, sipReason));
+    }
+
+    /**
      * @return Whether or not SipTransports are supported on the connected ImsService. This can
      * change based on the capabilities of the ImsService.
      * @throws ImsException if the ImsService connected to this controller is currently down.
@@ -366,11 +379,13 @@
             ISipDelegateMessageCallback delegateMessage,
             Consumer<ImsException> startedErrorConsumer) {
         ISipTransport transport;
+        IImsRegistration registration;
         // 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();
+            registration = mRcsManager.getImsRegistration();
             if (transport == null) {
                 logw("createSipDelegateInternal, transport null during request.");
                 startedErrorConsumer.accept(new ImsException("SipTransport not supported",
@@ -387,7 +402,7 @@
         }
 
         SipDelegateController c = mDelegateControllerFactory.create(subId, request, packageName,
-                transport, mExecutorService, delegateState, delegateMessage);
+                transport, registration, mExecutorService, delegateState, delegateMessage);
         logi("createSipDelegateInternal: request= " + request + ", packageName= " + packageName
                 + ", controller created: " + c);
         addPendingCreateAndEvaluate(c);
@@ -420,43 +435,98 @@
         addPendingDestroyAndEvaluate(match, reason);
     }
 
+    private void triggerFullNetworkRegistrationInternal(int subId, ISipDelegate connection,
+            int sipCode, String sipReason) {
+        if (subId != mSubId) {
+            logw("triggerFullNetworkRegistrationInternal: ignoring network reg request, as this is"
+                    + "about to be destroyed anyway due to subId change, delegate=" + connection);
+            return;
+        }
+        if (connection == null) {
+            logw("triggerFullNetworkRegistrationInternal: ignoring, null connection binder.");
+            return;
+        }
+        // Ensure the requester has a valid SipDelegate registered.
+        SipDelegateController match = null;
+        for (SipDelegateController controller : mDelegatePriorityQueue) {
+            if (Objects.equal(connection.asBinder(),
+                    controller.getSipDelegateInterface().asBinder())) {
+                match = controller;
+                break;
+            }
+        }
+        if (match == null) {
+            logw("triggerFullNetworkRegistrationInternal: could not find matching connection, "
+                    + "ignoring");
+            return;
+        }
+
+        match.triggerFullNetworkRegistration(sipCode, sipReason);
+    }
+
     /**
      * 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
+        cancelPendingUpdateRegistration();
+
+        IImsRegistration registrationImpl = mRcsManager.getImsRegistration();
+        if (registrationImpl != null) {
+            try {
+                registrationImpl.triggerSipDelegateDeregistration();
+            } catch (RemoteException e) {
+                logi("triggerDeregistrationEvent: received RemoteException: " + e);
+            }
+        }
     }
 
     /**
      * Schedule an update to the IMS registration. If there is an existing update scheduled, cancel
      * it and reschedule.
+     * <p>
+     * We want to wait because this can directly result in changes to the IMS registration on the
+     * network, so we need to wait for a steady state where all changes have been made before
+     * triggering an update to the network registration.
      */
     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);
-        }
+        cancelPendingUpdateRegistration();
+
         ScheduledFuture<?> f = mExecutorService.schedule(this::triggerUpdateRegistrationEvent,
                 mTimerAdapter.getUpdateRegistrationDelayMilliseconds(), TimeUnit.MILLISECONDS);
         logi("scheduleUpdateRegistration: scheduling new event: " + f);
         mPendingUpdateRegistrationFuture = f;
     }
 
+    /**
+     * Cancel an existing pending task to update the IMS registration associated with SIP delegates.
+     */
+    private void cancelPendingUpdateRegistration() {
+        if (mPendingUpdateRegistrationFuture == null
+                || mPendingUpdateRegistrationFuture.isDone()) {
+            return;
+        }
+        // Cancel the old pending operation and reschedule again.
+        mPendingUpdateRegistrationFuture.cancel(false);
+        logi("scheduleUpdateRegistration: cancelling existing reg update event: "
+                + mPendingUpdateRegistrationFuture);
+    }
+
+    /**
+     * Triggers an event to update the IMS registration of the ImsService. Should only be called
+     * from {@link #scheduleUpdateRegistration()}.
+     */
     private void triggerUpdateRegistrationEvent() {
         logi("triggerUpdateRegistrationEvent: Sending update registration event to ImsService");
-        //TODO hook up registration apis
+        IImsRegistration registrationImpl = mRcsManager.getImsRegistration();
+        if (registrationImpl != null) {
+            try {
+                registrationImpl.triggerUpdateSipDelegateRegistration();
+            } catch (RemoteException e) {
+                logi("triggerUpdateRegistrationEvent: received RemoteException: " + e);
+            }
+        }
     }
 
     /**
@@ -564,6 +634,9 @@
      * by another delegate higher in the priority queue.
      */
     private void reevaluateDelegates() {
+        // We need to cancel the pending update now and reschedule IMS registration update for
+        // after the reevaluate is complete.
+        cancelPendingUpdateRegistration();
         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.
@@ -614,10 +687,13 @@
             }, mExecutorService);
         }
 
-        // Executor doesn't matter here, just adding an extra stage to print result.
+        // Executor doesn't matter here, schedule an event to update the IMS registration.
         mEvaluateCompleteFuture = pendingChange
-                .thenAccept((associatedFeatures) -> logi("reevaluateDelegates: reevaluate complete,"
-                        + " feature tags associated: " + associatedFeatures));
+                .thenAccept((associatedFeatures) -> {
+                    logi("reevaluateDelegates: reevaluate complete," + " feature tags associated: "
+                            + associatedFeatures);
+                    scheduleUpdateRegistration();
+                });
         logi("reevaluateDelegates: future created.");
     }
 
@@ -666,13 +742,19 @@
             // CarrierConfigManager
             return;
         }
-        updateRoleCache();
-        // new denied tags will be picked up when reevaluate completes.
-        scheduleThrottledReevaluate();
+        boolean roleChanged = updateRoleCache();
+        if (roleChanged) {
+            triggerDeregistrationEvent();
+            // new denied tags will be picked up when reevaluate completes.
+            scheduleThrottledReevaluate();
+        }
     }
 
 
-    private void updateRoleCache() {
+    /**
+     * @return true, if the role cache has changed, false otherwise.
+     */
+    private boolean updateRoleCache() {
         String newSmsRolePackageName = "";
         try {
             // Only one app can fulfill the SMS role.
@@ -685,9 +767,10 @@
         logi("updateRoleCache: new packageName=" + newSmsRolePackageName);
         if (TextUtils.equals(mCachedSmsRolePackageName, newSmsRolePackageName)) {
             logi("updateRoleCache, skipping, role did not change");
-            return;
+            return false;
         }
         mCachedSmsRolePackageName = newSmsRolePackageName;
+        return true;
     }
 
     /**
diff --git a/tests/src/com/android/TelephonyTestBase.java b/tests/src/com/android/TelephonyTestBase.java
index 502740d..09abb15 100644
--- a/tests/src/com/android/TelephonyTestBase.java
+++ b/tests/src/com/android/TelephonyTestBase.java
@@ -61,9 +61,7 @@
 
     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) {
diff --git a/tests/src/com/android/services/telephony/rcs/SipDelegateBinderConnectionTest.java b/tests/src/com/android/services/telephony/rcs/SipDelegateBinderConnectionTest.java
index fa439dc..360fa21 100644
--- a/tests/src/com/android/services/telephony/rcs/SipDelegateBinderConnectionTest.java
+++ b/tests/src/com/android/services/telephony/rcs/SipDelegateBinderConnectionTest.java
@@ -32,6 +32,7 @@
 import android.telephony.ims.FeatureTagState;
 import android.telephony.ims.SipDelegateImsConfiguration;
 import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.aidl.IImsRegistration;
 import android.telephony.ims.aidl.ISipDelegate;
 import android.telephony.ims.aidl.ISipDelegateMessageCallback;
 import android.telephony.ims.aidl.ISipDelegateStateCallback;
@@ -61,6 +62,7 @@
 
     @Mock private ISipDelegate mMockDelegate;
     @Mock private ISipTransport mMockTransport;
+    @Mock private IImsRegistration mMockRegistration;
     @Mock private IBinder mTransportBinder;
     @Mock private ISipDelegateMessageCallback mMessageCallback;
     @Mock private DelegateBinderStateManager.StateCallback mMockStateCallback;
@@ -107,7 +109,8 @@
         DelegateRequest request = getDelegateRequest();
         ArraySet<FeatureTagState> deniedTags = getMmTelDeniedTag();
         SipDelegateBinderConnection connection = new SipDelegateBinderConnection(TEST_SUB_ID,
-                mMockTransport, request, deniedTags, Runnable::run, mStateCallbackList);
+                mMockTransport, mMockRegistration, request, deniedTags, Runnable::run,
+                mStateCallbackList);
         ISipDelegateStateCallback cb = createDelegateCaptureStateCallback(request, connection);
 
         // Send onCreated callback from SipDelegate
@@ -130,7 +133,8 @@
         DelegateRequest request = getDelegateRequest();
         ArraySet<FeatureTagState> deniedTags = getMmTelDeniedTag();
         SipDelegateBinderConnection connection = new SipDelegateBinderConnection(TEST_SUB_ID,
-                mMockTransport, request, deniedTags, Runnable::run, mStateCallbackList);
+                mMockTransport, mMockRegistration, request, deniedTags, Runnable::run,
+                mStateCallbackList);
         doThrow(new RemoteException()).when(mMockTransport).createSipDelegate(eq(TEST_SUB_ID),
                 any(), any(), any());
         ISipDelegateStateCallback cb = createDelegateCaptureStateCallback(request, connection);
@@ -143,7 +147,8 @@
         DelegateRequest request = getDelegateRequest();
         ArraySet<FeatureTagState> deniedTags = getMmTelDeniedTag();
         SipDelegateBinderConnection connection = new SipDelegateBinderConnection(TEST_SUB_ID,
-                mMockTransport, request, deniedTags, Runnable::run, mStateCallbackList);
+                mMockTransport, mMockRegistration, request, deniedTags, Runnable::run,
+                mStateCallbackList);
         ISipDelegateStateCallback cb = createDelegateCaptureStateCallback(request, connection);
         assertNotNull(cb);
         cb.onCreated(mMockDelegate, null /*denied*/);
@@ -162,7 +167,8 @@
         DelegateRequest request = getDelegateRequest();
         ArraySet<FeatureTagState> deniedTags = getMmTelDeniedTag();
         SipDelegateBinderConnection connection = new SipDelegateBinderConnection(TEST_SUB_ID,
-                mMockTransport, request, deniedTags, Runnable::run, mStateCallbackList);
+                mMockTransport, mMockRegistration, request, deniedTags, Runnable::run,
+                mStateCallbackList);
         ISipDelegateStateCallback cb = createDelegateCaptureStateCallback(request, connection);
         assertNotNull(cb);
         cb.onCreated(mMockDelegate, null /*denied*/);
@@ -181,7 +187,8 @@
         DelegateRequest request = getDelegateRequest();
         ArraySet<FeatureTagState> deniedTags = getMmTelDeniedTag();
         SipDelegateBinderConnection connection = new SipDelegateBinderConnection(TEST_SUB_ID,
-                mMockTransport, request, deniedTags, Runnable::run, mStateCallbackList);
+                mMockTransport, mMockRegistration, request, deniedTags, Runnable::run,
+                mStateCallbackList);
         ISipDelegateStateCallback cb = createDelegateCaptureStateCallback(request, connection);
         assertNotNull(cb);
         cb.onCreated(mMockDelegate, new ArrayList<>(deniedTags));
diff --git a/tests/src/com/android/services/telephony/rcs/SipDelegateControllerTest.java b/tests/src/com/android/services/telephony/rcs/SipDelegateControllerTest.java
index 47b4808..27f896b 100644
--- a/tests/src/com/android/services/telephony/rcs/SipDelegateControllerTest.java
+++ b/tests/src/com/android/services/telephony/rcs/SipDelegateControllerTest.java
@@ -36,7 +36,6 @@
 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;
@@ -65,7 +64,6 @@
     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;
@@ -243,9 +241,9 @@
 
     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) ->  {
+        return new SipDelegateController(TEST_SUB_ID, request, "", mExecutorService,
+                mMockMessageTracker, mMockDelegateStateTracker,
+                (a, b, deniedFeatureSet, d, e) ->  {
                     assertEquals(deniedSet, deniedFeatureSet);
                     return mMockBinderConnection;
                 });
diff --git a/tests/src/com/android/services/telephony/rcs/SipTransportControllerTest.java b/tests/src/com/android/services/telephony/rcs/SipTransportControllerTest.java
index 8e10757..fa27775 100644
--- a/tests/src/com/android/services/telephony/rcs/SipTransportControllerTest.java
+++ b/tests/src/com/android/services/telephony/rcs/SipTransportControllerTest.java
@@ -40,6 +40,7 @@
 import android.telephony.ims.FeatureTagState;
 import android.telephony.ims.ImsException;
 import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.aidl.IImsRegistration;
 import android.telephony.ims.aidl.ISipDelegate;
 import android.telephony.ims.aidl.ISipDelegateConnectionStateCallback;
 import android.telephony.ims.aidl.ISipDelegateMessageCallback;
@@ -102,6 +103,7 @@
 
     @Mock private RcsFeatureManager mRcsManager;
     @Mock private ISipTransport mSipTransport;
+    @Mock private IImsRegistration mImsRegistration;
     @Mock private ISipDelegateConnectionStateCallback mMockStateCallback;
     @Mock private ISipDelegateMessageCallback mMockMessageCallback;
     @Mock private SipTransportController.SipDelegateControllerFactory
@@ -116,6 +118,7 @@
     public void setUp() throws Exception {
         super.setUp();
         doReturn(mSmsPackageName).when(mMockRoleManager).getRoleHolders(RoleManager.ROLE_SMS);
+        doReturn(mImsRegistration).when(mRcsManager).getImsRegistration();
         mSmsPackageName.add(TEST_PACKAGE_NAME);
         doAnswer(invocation -> {
             Integer subId = invocation.getArgument(0);
@@ -124,7 +127,7 @@
             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(),
+        }).when(mMockDelegateControllerFactory).create(anyInt(), any(), anyString(), any(), any(),
                 any(), any(), any());
     }
 
@@ -289,6 +292,8 @@
         SipDelegateController c = injectMockDelegateController(TEST_PACKAGE_NAME, r);
         createDelegateAndVerify(controller, c, r, r.getFeatureTags(), Collections.emptySet(),
                 TEST_PACKAGE_NAME);
+        verifyDelegateRegistrationChangedEvent(1 /*times*/, 0 /*waitMs*/);
+        triggerFullNetworkRegistrationAndVerify(controller, c);
     }
 
     @SmallTest
@@ -300,9 +305,12 @@
         SipDelegateController c = injectMockDelegateController(TEST_PACKAGE_NAME, r);
         createDelegateAndVerify(controller, c, r, r.getFeatureTags(), Collections.emptySet(),
                 TEST_PACKAGE_NAME);
+        verifyDelegateRegistrationChangedEvent(1, 0 /*throttle*/);
 
         destroyDelegateAndVerify(controller, c, false,
                 SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+        verifyDelegateRegistrationChangedEvent(2 /*times*/, 0 /*waitMs*/);
+        triggerFullNetworkRegistrationAndVerifyNever(controller, c);
     }
 
     @SmallTest
@@ -323,7 +331,8 @@
     @SmallTest
     @Test
     public void createTwoAndDenyOverlappingTags() throws Exception {
-        SipTransportController controller = setupLiveTransportController();
+        SipTransportController controller = setupLiveTransportController(0 /*reeval*/,
+                THROTTLE_MS);
 
         // First delegate requests RCS message + File transfer
         ArraySet<String> firstDelegate = new ArraySet<>(getBaseDelegateRequest().getFeatureTags());
@@ -333,6 +342,8 @@
                 firstDelegateRequest);
         createDelegateAndVerify(controller, c1, firstDelegateRequest, firstDelegate,
                 Collections.emptySet(), TEST_PACKAGE_NAME);
+        // there is a delay in the indication to update reg, so it should not happen yet.
+        verifyNoDelegateRegistrationChangedEvent();
 
         // First delegate requests RCS message + Group RCS message. For this delegate, single RCS
         // message should be denied.
@@ -346,12 +357,14 @@
                 secondDelegateRequest);
         createDelegateAndVerify(controller, c2, secondDelegateRequest, grantedAndDenied.first,
                 grantedAndDenied.second, TEST_PACKAGE_NAME, 1);
+        // a reg changed event should happen after wait.
+        verifyDelegateRegistrationChangedEvent(1, 2 * THROTTLE_MS);
     }
 
     @SmallTest
     @Test
     public void createTwoAndTriggerRoleChange() throws Exception {
-        SipTransportController controller = setupLiveTransportController();
+        SipTransportController controller = setupLiveTransportController(0 /*reeval*/, THROTTLE_MS);
 
         DelegateRequest firstDelegateRequest = getBaseDelegateRequest();
         Set<FeatureTagState> firstDeniedTags = getDeniedTagsForReason(
@@ -361,6 +374,7 @@
                 firstDelegateRequest);
         createDelegateAndVerify(controller, c1, firstDelegateRequest,
                 firstDelegateRequest.getFeatureTags(), Collections.emptySet(), TEST_PACKAGE_NAME);
+        verifyDelegateRegistrationChangedEvent(1 /*times*/, THROTTLE_MS);
 
         DelegateRequest secondDelegateRequest = getBaseDelegateRequest();
         Set<FeatureTagState> secondDeniedTags = getDeniedTagsForReason(
@@ -378,6 +392,10 @@
         CompletableFuture<Boolean> pendingC2Change = setChangeSupportedFeatureTagsFuture(c2,
                 secondDelegateRequest.getFeatureTags(), Collections.emptySet());
         setSmsRoleAndEvaluate(controller, TEST_PACKAGE_NAME_2);
+        // swapping roles should trigger a deregistration event on the ImsService side.
+        verifyDelegateDeregistrationEvent();
+        // there should also not be any new registration changed events
+        verifyDelegateRegistrationChangedEvent(1 /*times*/, THROTTLE_MS);
         // trigger completion stage to run
         waitForExecutorAction(mExecutorService, TIMEOUT_MS);
         verify(c1).changeSupportedFeatureTags(Collections.emptySet(), firstDeniedTags);
@@ -394,12 +412,14 @@
         // ensure we are not blocking executor here
         waitForExecutorAction(mExecutorService, TIMEOUT_MS);
         completePendingChange(pendingC2Change, true);
+        // verify we now get a second registration changed event
+        verifyDelegateRegistrationChangedEvent(2 /*times*/, THROTTLE_MS);
     }
 
     @SmallTest
     @Test
     public void createTwoAndDestroyOlder() throws Exception {
-        SipTransportController controller = setupLiveTransportController();
+        SipTransportController controller = setupLiveTransportController(0 /*reeval*/, THROTTLE_MS);
 
         // First delegate requests RCS message + File transfer
         ArraySet<String> firstDelegate = new ArraySet<>(getBaseDelegateRequest().getFeatureTags());
@@ -409,6 +429,7 @@
                 firstDelegateRequest);
         createDelegateAndVerify(controller, c1, firstDelegateRequest, firstDelegate,
                 Collections.emptySet(), TEST_PACKAGE_NAME);
+        verifyNoDelegateRegistrationChangedEvent();
 
         // First delegate requests RCS message + Group RCS message. For this delegate, single RCS
         // message should be denied.
@@ -422,6 +443,7 @@
                 secondDelegateRequest);
         createDelegateAndVerify(controller, c2, secondDelegateRequest, grantedAndDenied.first,
                 grantedAndDenied.second, TEST_PACKAGE_NAME, 1);
+        verifyNoDelegateRegistrationChangedEvent();
 
         // Destroy the firstDelegate, which should now cause all previously denied tags to be
         // granted to the new delegate.
@@ -433,12 +455,14 @@
         assertTrue(waitForExecutorAction(mExecutorService, TIMEOUT_MS));
         verify(c2).changeSupportedFeatureTags(secondDelegate, Collections.emptySet());
         completePendingChange(pendingC2Change, true);
+
+        verifyDelegateRegistrationChangedEvent(1 /*times*/, THROTTLE_MS);
     }
 
     @SmallTest
     @Test
     public void testThrottling() throws Exception {
-        SipTransportController controller = setupLiveTransportController(THROTTLE_MS);
+        SipTransportController controller = setupLiveTransportController(THROTTLE_MS, THROTTLE_MS);
 
         // First delegate requests RCS message + File transfer
         ArraySet<String> firstDelegate = new ArraySet<>(getBaseDelegateRequest().getFeatureTags());
@@ -477,12 +501,14 @@
                 thirdDelegateRequest, grantedAndDeniedC3.first, grantedAndDeniedC3.second,
                 TEST_PACKAGE_NAME);
 
+        verifyNoDelegateRegistrationChangedEvent();
         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);
+        verifyDelegateRegistrationChangedEvent(1, 2 * THROTTLE_MS);
 
         // Destroy the first and second controller in quick succession, this should only generate
         // one reevaluate for the third controller.
@@ -505,6 +531,7 @@
         verify(c3).changeSupportedFeatureTags(thirdDelegate, Collections.emptySet());
         // In total reeval should have only been called twice.
         verify(c3, times(2)).changeSupportedFeatureTags(any(), any());
+        verifyDelegateRegistrationChangedEvent(2 /*times*/, 2 * THROTTLE_MS);
     }
 
     @SmallTest
@@ -518,6 +545,7 @@
                 firstDelegateRequest);
         createDelegateAndVerify(controller, c1, firstDelegateRequest, firstDelegate,
                 Collections.emptySet(), TEST_PACKAGE_NAME);
+        verifyDelegateRegistrationChangedEvent(1 /*times*/, 0 /*waitMs*/);
 
         CompletableFuture<Integer> pendingDestroy =  setDestroyFuture(c1, true,
                 SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SUBSCRIPTION_TORN_DOWN);
@@ -525,6 +553,7 @@
         waitForExecutorAction(mExecutorService, TIMEOUT_MS);
         verifyDestroyDelegate(controller, c1, pendingDestroy, true /*force*/,
                 SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SUBSCRIPTION_TORN_DOWN);
+        verifyDelegateRegistrationChangedEvent(2 /*times*/, 0 /*waitMs*/);
     }
 
     @SmallTest
@@ -545,6 +574,7 @@
         waitForExecutorAction(mExecutorService, TIMEOUT_MS);
         verifyDestroyDelegate(controller, c1, pendingDestroy, true /*force*/,
                 SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SERVICE_DEAD);
+        verifyDelegateRegistrationChangedEvent(1, 0 /*waitMs*/);
     }
 
     @SmallTest
@@ -563,6 +593,7 @@
                 SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SUBSCRIPTION_TORN_DOWN);
         controller.onDestroy();
         waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+        verifyDelegateDeregistrationEvent();
         // verify change was called.
         verify(c1).destroy(true /*force*/,
                 SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SUBSCRIPTION_TORN_DOWN);
@@ -575,7 +606,7 @@
     @SmallTest
     @Test
     public void testTimingSubIdChangedAndCreateNewSubId() throws Exception {
-        SipTransportController controller = setupLiveTransportController(THROTTLE_MS);
+        SipTransportController controller = setupLiveTransportController(THROTTLE_MS, 0);
 
         ArraySet<String> firstDelegate = new ArraySet<>(getBaseDelegateRequest().getFeatureTags());
         DelegateRequest firstDelegateRequest = new DelegateRequest(firstDelegate);
@@ -638,13 +669,14 @@
     }
 
     private SipTransportController setupLiveTransportController() throws Exception {
-        return setupLiveTransportController(0 /*throttleMs*/);
+        return setupLiveTransportController(0 /*throttleMs*/, 0 /*regDelayMs*/);
     }
 
-    private SipTransportController setupLiveTransportController(int throttleMs) throws Exception {
+    private SipTransportController setupLiveTransportController(int throttleMs, int regDelayMs)
+            throws Exception {
         mExecutorService = Executors.newSingleThreadScheduledExecutor();
         SipTransportController controller = createControllerAndThrottle(mExecutorService,
-                throttleMs);
+                throttleMs, regDelayMs);
         doReturn(mSipTransport).when(mRcsManager).getSipTransport();
         controller.onAssociatedSubscriptionUpdated(TEST_SUB_ID);
         controller.onRcsConnected(mRcsManager);
@@ -743,6 +775,24 @@
         completePendingDestroy(pendingDestroy, reason);
     }
 
+    private void triggerFullNetworkRegistrationAndVerify(SipTransportController controller,
+            SipDelegateController delegateController) {
+        controller.triggerFullNetworkRegistration(TEST_SUB_ID,
+                delegateController.getSipDelegateInterface(), 403, "forbidden");
+        // move to internal & trigger event
+        waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+        verify(delegateController).triggerFullNetworkRegistration(403, "forbidden");
+    }
+
+    private void triggerFullNetworkRegistrationAndVerifyNever(SipTransportController controller,
+            SipDelegateController delegateController) {
+        controller.triggerFullNetworkRegistration(TEST_SUB_ID,
+                delegateController.getSipDelegateInterface(), 403, "forbidden");
+        // move to internal & potentially trigger event
+        waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+        verify(delegateController, never()).triggerFullNetworkRegistration(anyInt(), anyString());
+    }
+
     private DelegateRequest getBaseDelegateRequest() {
         Set<String> featureTags = new ArraySet<>();
         featureTags.add(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG);
@@ -807,12 +857,31 @@
         waitForExecutorAction(mExecutorService, TIMEOUT_MS);
     }
 
+    private void verifyNoDelegateRegistrationChangedEvent() throws Exception {
+        // event is scheduled and then executed.
+        waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+        verify(mImsRegistration, never()).triggerUpdateSipDelegateRegistration();
+    }
+
+    private void verifyDelegateRegistrationChangedEvent(int times, int waitMs)
+            throws Exception {
+        // event is scheduled and then executed.
+        assertTrue(scheduleDelayedWait(waitMs));
+        waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+        verify(mImsRegistration, times(times)).triggerUpdateSipDelegateRegistration();
+    }
+
+
+    private void verifyDelegateDeregistrationEvent() throws Exception {
+        verify(mImsRegistration).triggerSipDelegateDeregistration();
+    }
+
     private SipTransportController createController(ScheduledExecutorService e) {
-        return createControllerAndThrottle(e, 0 /*throttleMs*/);
+        return createControllerAndThrottle(e, 0 /*throttleMs*/, 0 /*regDelayMs*/);
     }
 
     private SipTransportController createControllerAndThrottle(ScheduledExecutorService e,
-            int throttleMs) {
+            int throttleMs, int regDelayMs) {
         return new SipTransportController(mContext, 0 /*slotId*/, TEST_SUB_ID,
                 mMockDelegateControllerFactory, mMockRoleManager,
                 // Remove delays for testing.
@@ -824,7 +893,7 @@
 
                     @Override
                     public int getUpdateRegistrationDelayMilliseconds() {
-                        return 0;
+                        return regDelayMs;
                     }
                 }, e);
     }