Merge "Introduce SIP message validation for SIP Delegates (1/2)" am: 33305bb3fd am: c338c769a2 am: 9cbc5ed9f8

Original change: https://android-review.googlesource.com/c/platform/packages/services/Telephony/+/1646653

Change-Id: I5163bb85e9ee5998207a392753bb484c6941936c
diff --git a/src/com/android/services/telephony/rcs/MessageTransportStateTracker.java b/src/com/android/services/telephony/rcs/MessageTransportWrapper.java
similarity index 65%
rename from src/com/android/services/telephony/rcs/MessageTransportStateTracker.java
rename to src/com/android/services/telephony/rcs/MessageTransportWrapper.java
index e640735..159e3e7 100644
--- a/src/com/android/services/telephony/rcs/MessageTransportStateTracker.java
+++ b/src/com/android/services/telephony/rcs/MessageTransportWrapper.java
@@ -31,69 +31,32 @@
 import android.util.LocalLog;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.SipMessageParsingUtils;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.services.telephony.rcs.validator.ValidationResult;
+
 import java.io.PrintWriter;
+import java.util.List;
 import java.util.Set;
 import java.util.concurrent.Executor;
+import java.util.concurrent.ScheduledExecutorService;
 import java.util.function.Consumer;
 
 /**
- * Tracks the SIP message path both from the IMS application to the SipDelegate and from the
+ * Wraps 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.
+ * Queues incoming and outgoing SIP messages on an Executor and deliver to IMS application and
+ * SipDelegate in order. If there is an error delivering the message, the caller is notified.
+ * Uses {@link TransportSipSessionTracker} to track ongoing SIP dialogs and verify outgoing
+ * messages.
  * <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;
-        }
-    }
+public class MessageTransportWrapper implements DelegateBinderStateManager.StateCallback {
+    private static final String TAG = "MessageTW";
 
     // SipDelegateConnection(IMS Application) -> SipDelegate(ImsService)
     private final ISipDelegate.Stub mSipDelegateConnection = new ISipDelegate.Stub() {
@@ -113,8 +76,7 @@
                         return;
                     }
                     try {
-                        // TODO track the SIP Dialogs created/destroyed on the associated
-                        // SipDelegate.
+                        mSipSessionTracker.acknowledgePendingMessage(viaTransactionId);
                         mSipDelegate.notifyMessageReceived(viaTransactionId);
                     } catch (RemoteException e) {
                         logw("SipDelegate not available when notifyMessageReceived was called "
@@ -142,8 +104,7 @@
                         return;
                     }
                     try {
-                        // TODO track the SIP Dialogs created/destroyed on the associated
-                        // SipDelegate.
+                        mSipSessionTracker.notifyPendingMessageFailed(viaTransactionId);
                         mSipDelegate.notifyMessageReceiveError(viaTransactionId, reason);
                     } catch (RemoteException e) {
                         logw("SipDelegate not available when notifyMessageReceiveError was called "
@@ -164,18 +125,23 @@
             long token = Binder.clearCallingIdentity();
             try {
                 mExecutor.execute(() -> {
-                    VerificationResult result = verifyOutgoingMessage(sipMessage);
-                    if (!result.isVerified) {
+                    ValidationResult result =
+                            mSipSessionTracker.verifyOutgoingMessage(sipMessage, configVersion);
+                    if (!result.isValidated) {
                         notifyDelegateSendError("Outgoing messages restricted", sipMessage,
                                 result.restrictedReason);
                         return;
                     }
                     try {
-                        // TODO track the SIP Dialogs created/destroyed on the associated
-                        // SipDelegate.
+                        if (mSipDelegate == null) {
+                            logw("sendMessage called when SipDelegate is not associated."
+                                    + sipMessage);
+                            notifyDelegateSendError("No SipDelegate", sipMessage,
+                                    SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_DEAD);
+
+                            return;
+                        }
                         mSipDelegate.sendMessage(sipMessage, configVersion);
-                        logi("sendMessage: message sent - " + sipMessage + ", configVersion: "
-                                + configVersion);
                     } catch (RemoteException e) {
                         notifyDelegateSendError("RemoteException: " + e, sipMessage,
                                 SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_DEAD);
@@ -194,21 +160,7 @@
         public void cleanupSession(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.cleanupSession(callId);
-                    } catch (RemoteException e) {
-                        logw("SipDelegate not available when closeDialog was called "
-                                + "for call id: " + callId);
-                    }
-                });
+                mExecutor.execute(() -> cleanupSessionInternal(callId));
             } finally {
                 Binder.restoreCallingIdentity(token);
             }
@@ -230,17 +182,14 @@
             long token = Binder.clearCallingIdentity();
             try {
                 mExecutor.execute(() -> {
-                    VerificationResult result = verifyIncomingMessage(message);
-                    if (!result.isVerified) {
+                    ValidationResult result = mSipSessionTracker.verifyIncomingMessage(message);
+                    if (!result.isValidated) {
                         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);
@@ -253,7 +202,7 @@
 
         /**
          * An outgoing SIP message sent previously by the SipDelegateConnection to the SipDelegate
-         * using {@link ISipDelegate#sendMessage(SipMessage, int)} as been successfully sent.
+         * using {@link ISipDelegate#sendMessage(SipMessage, long)} as been successfully sent.
          */
         @Override
         public void onMessageSent(String viaTransactionId) {
@@ -265,6 +214,7 @@
                                 + "associated");
                     }
                     try {
+                        mSipSessionTracker.acknowledgePendingMessage(viaTransactionId);
                         mAppCallback.onMessageSent(viaTransactionId);
                     } catch (RemoteException e) {
                         logw("Error sending onMessageSent to SipDelegateConnection, remote not"
@@ -278,7 +228,7 @@
 
         /**
          * An outgoing SIP message sent previously by the SipDelegateConnection to the SipDelegate
-         * using {@link ISipDelegate#sendMessage(SipMessage, int)} failed to be sent.
+         * using {@link ISipDelegate#sendMessage(SipMessage, long)} failed to be sent.
          */
         @Override
         public void onMessageSendFailure(String viaTransactionId, int reason) {
@@ -290,6 +240,7 @@
                                 + "associated");
                     }
                     try {
+                        mSipSessionTracker.notifyPendingMessageFailed(viaTransactionId);
                         mAppCallback.onMessageSendFailure(viaTransactionId, reason);
                     } catch (RemoteException e) {
                         logw("Error sending onMessageSendFailure to SipDelegateConnection, remote"
@@ -305,52 +256,77 @@
     private final ISipDelegateMessageCallback mAppCallback;
     private final Executor mExecutor;
     private final int mSubId;
+    private final TransportSipSessionTracker mSipSessionTracker;
     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,
+    public MessageTransportWrapper(int subId, ScheduledExecutorService executor,
             ISipDelegateMessageCallback appMessageCallback) {
         mSubId = subId;
         mAppCallback = appMessageCallback;
         mExecutor = executor;
+        mSipSessionTracker = new TransportSipSessionTracker(subId, executor);
+    }
+
+    /**
+     * Mock out dependencies for unit testing.
+     */
+    @VisibleForTesting
+    public MessageTransportWrapper(int subId, ScheduledExecutorService executor,
+            ISipDelegateMessageCallback appMessageCallback,
+            TransportSipSessionTracker sipSessionTracker) {
+        mSubId = subId;
+        mAppCallback = appMessageCallback;
+        mExecutor = executor;
+        mSipSessionTracker = sipSessionTracker;
     }
 
     @Override
     public void onRegistrationStateChanged(DelegateRegistrationState registrationState) {
-        // TODO: integrate registration changes to SipMessage verification checks.
+        mSipSessionTracker.onRegistrationStateChanged((List<String> callIds) -> {
+            for (String id : callIds)  {
+                cleanupSessionInternal(id);
+            }
+        }, registrationState);
     }
 
     @Override
     public void onImsConfigurationChanged(SipDelegateImsConfiguration config) {
-        // Not needed for this Tracker
+        mSipSessionTracker.onImsConfigurationChanged(config);
     }
 
     @Override
     public void onConfigurationChanged(SipDelegateConfiguration config) {
-        // Not needed for this Tracker
+        mSipSessionTracker.onConfigurationChanged(config);
     }
 
     /**
      * 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 supportedFeatureTags Feature tags that are supported. Outgoing SIP messages relating
+     *                             to these tags will be allowed.
      * @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) {
+    public void openTransport(ISipDelegate delegate, Set<String> supportedFeatureTags,
+            Set<FeatureTagState> deniedFeatureTags) {
+        logi("openTransport: delegate=" + delegate + ", supportedTags=" + supportedFeatureTags
+                + ", deniedTags=" + deniedFeatureTags);
+        mSipSessionTracker.onTransportOpened(supportedFeatureTags, 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) {
-        printWriter.println("Most recent logs:");
+        IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, "  ");
+        pw.println("Most recent logs:");
         mLocalLog.dump(printWriter);
+        pw.println();
+        pw.println("Dialog Tracker:");
+        pw.increaseIndent();
+        mSipSessionTracker.dump(pw);
+        pw.decreaseIndent();
     }
 
     /**
@@ -400,15 +376,15 @@
      */
     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);
-        });
+        logi("closeGracefully: closingReason=" + delegateClosingReason + ", closedReason="
+                + closedReason + ", resultConsumer(" + resultConsumer.hashCode() + ")");
+        mSipSessionTracker.closeSessionsGracefully((openCallIds) -> {
+            logi("closeGracefully resultConsumer(" + resultConsumer.hashCode()
+                    + "): open call IDs:{" + openCallIds + "}");
+            closeTransport(openCallIds);
+            // propagate event to the consumer
+            resultConsumer.accept(openCallIds.isEmpty() /*successfullyClosed*/);
+        }, delegateClosingReason, closedReason);
     }
 
     /**
@@ -418,63 +394,53 @@
      *         if an attempt is made to send/receive a message after this method is called.
      */
     public void close(int closedReason) {
-        closeTransport(closedReason);
+        List<String> openDialogs = mSipSessionTracker.closeSessionsForcefully(closedReason);
+        logi("close: closedReason=" + closedReason + "open call IDs:{" + openDialogs + "}");
+        closeTransport(openDialogs);
     }
 
     // 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.
+    private void closeTransport(List<String> openCallIds) {
+        for (String id : openCallIds) {
+            cleanupSessionInternal(id);
+        }
         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);
-        }
+    private void cleanupSessionInternal(String callId) {
+        logi("cleanupSessionInternal: closing session with callId: " + callId);
+        mSipSessionTracker.onSipSessionCleanup(callId);
+
         if (mSipDelegate == null) {
-            logw("sendMessage called when SipDelegate is not associated." + message);
-            return new VerificationResult(SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_DEAD);
+            logw("cleanupSession called when SipDelegate is not associated, callId: "
+                    + callId);
+            return;
         }
-        return VerificationResult.SUCCESS;
-    }
-
-    private VerificationResult verifyIncomingMessage(SipMessage message) {
-        // Do not restrict incoming based on closing reason.
-        if (mDelegateClosedReason > -1) {
-            return new VerificationResult(mDelegateClosedReason);
+        try {
+            mSipDelegate.cleanupSession(callId);
+        } catch (RemoteException e) {
+            logw("SipDelegate not available when cleanupSession was called "
+                    + "for call id: " + callId);
         }
-        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);
+        String transactionId = SipMessageParsingUtils.getTransactionId(message.getHeaderSection());
+        logi("Error sending SipMessage[id: " + transactionId + ", code: " + reasonCode
+                + "] -> SipDelegate for reason: " + logReason);
         try {
-            mAppCallback.onMessageSendFailure(null, reasonCode);
+            mAppCallback.onMessageSendFailure(transactionId, 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 + "] -> "
+        String transactionId = SipMessageParsingUtils.getTransactionId(message.getHeaderSection());
+        logi("Error sending SipMessage[id: " + transactionId + ", code: " + reasonCode + "] -> "
                 + "SipDelegateConnection for reason: " + logReason);
         try {
-            mSipDelegate.notifyMessageReceiveError(null, reasonCode);
+            mSipDelegate.notifyMessageReceiveError(transactionId, reasonCode);
         } catch (RemoteException e) {
             logw("notifyAppReceiveError, SipDelegate is not available: " + e);
         }
diff --git a/src/com/android/services/telephony/rcs/SipDelegateBinderConnection.java b/src/com/android/services/telephony/rcs/SipDelegateBinderConnection.java
index 5eb0558..168a432 100644
--- a/src/com/android/services/telephony/rcs/SipDelegateBinderConnection.java
+++ b/src/com/android/services/telephony/rcs/SipDelegateBinderConnection.java
@@ -98,7 +98,8 @@
                     long token = Binder.clearCallingIdentity();
                     try {
                         mExecutor.execute(() -> {
-                            logi("onImsConfigurationChanged");
+                            logi("onImsConfigurationChanged: version="
+                                    + registeredSipConfig.getVersion());
                             for (StateCallback c : mStateCallbacks) {
                                 c.onImsConfigurationChanged(registeredSipConfig);
                             }
diff --git a/src/com/android/services/telephony/rcs/SipDelegateController.java b/src/com/android/services/telephony/rcs/SipDelegateController.java
index 8cd4365..c2e5e67 100644
--- a/src/com/android/services/telephony/rcs/SipDelegateController.java
+++ b/src/com/android/services/telephony/rcs/SipDelegateController.java
@@ -28,6 +28,7 @@
 import android.telephony.ims.aidl.ISipTransport;
 import android.telephony.ims.stub.DelegateConnectionStateCallback;
 import android.telephony.ims.stub.SipDelegate;
+import android.util.ArraySet;
 import android.util.LocalLog;
 import android.util.Log;
 import android.util.Pair;
@@ -42,6 +43,7 @@
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
 import java.util.concurrent.ScheduledExecutorService;
+import java.util.stream.Collectors;
 
 /**
  * Created when an IMS application wishes to open up a {@link SipDelegateConnection} and manages the
@@ -78,7 +80,7 @@
     private final String mPackageName;
     private final DelegateRequest mInitialRequest;
     private final ScheduledExecutorService mExecutorService;
-    private final MessageTransportStateTracker mMessageTransportStateTracker;
+    private final MessageTransportWrapper mMessageTransportWrapper;
     private final DelegateStateTracker mDelegateStateTracker;
     private final DelegateBinderStateManager.Factory mBinderConnectionFactory;
     private final LocalLog mLocalLog = new LocalLog(SipTransportController.LOG_SIZE);
@@ -97,10 +99,10 @@
         mExecutorService = executorService;
         mBinderConnectionFactory = new BinderConnectionFactory(transportImpl, registrationImpl);
 
-        mMessageTransportStateTracker = new MessageTransportStateTracker(mSubId, executorService,
+        mMessageTransportWrapper = new MessageTransportWrapper(mSubId, executorService,
                 messageCallback);
         mDelegateStateTracker = new DelegateStateTracker(mSubId, stateCallback,
-                mMessageTransportStateTracker.getDelegateConnection());
+                mMessageTransportWrapper.getDelegateConnection());
     }
 
     /**
@@ -109,14 +111,14 @@
     @VisibleForTesting
     public SipDelegateController(int subId, DelegateRequest initialRequest, String packageName,
             ScheduledExecutorService executorService,
-            MessageTransportStateTracker messageTransportStateTracker,
+            MessageTransportWrapper messageTransportWrapper,
             DelegateStateTracker delegateStateTracker,
             DelegateBinderStateManager.Factory connectionFactory) {
         mSubId = subId;
         mInitialRequest = initialRequest;
         mPackageName = packageName;
         mExecutorService = executorService;
-        mMessageTransportStateTracker = messageTransportStateTracker;
+        mMessageTransportWrapper = messageTransportWrapper;
         mDelegateStateTracker = delegateStateTracker;
         mBinderConnectionFactory = connectionFactory;
     }
@@ -140,14 +142,14 @@
      * @return The ImsService's SIP delegate binder impl associated with this controller.
      */
     public ISipDelegate getSipDelegateInterface() {
-        return mMessageTransportStateTracker.getDelegateConnection();
+        return mMessageTransportWrapper.getDelegateConnection();
     }
 
     /**
      * @return The IMS app's message callback binder.
      */
     public ISipDelegateMessageCallback getAppMessageCallback() {
-        return mMessageTransportStateTracker.getAppMessageCallback();
+        return mMessageTransportWrapper.getAppMessageCallback();
     }
 
     /**
@@ -183,7 +185,12 @@
             }
             mBinderConnection = connection;
             logi("create: created, delegate denied: " + resultPair.second);
-            mMessageTransportStateTracker.openTransport(resultPair.first, resultPair.second);
+            Set<String> allowedTags = new ArraySet<>(supportedSet);
+            // Start with the supported set and remove all tags that were denied.
+            allowedTags.removeAll(resultPair.second.stream().map(FeatureTagState::getFeatureTag)
+                    .collect(Collectors.toSet()));
+            mMessageTransportWrapper.openTransport(resultPair.first, allowedTags,
+                    resultPair.second);
             mDelegateStateTracker.sipDelegateConnected(resultPair.second);
             return true;
         });
@@ -323,10 +330,10 @@
         CompletableFuture<Boolean> pendingTransportClosed = new CompletableFuture<>();
         if (force) {
             logi("destroySipDelegate, forced");
-            mMessageTransportStateTracker.close(messageDestroyedReason);
+            mMessageTransportWrapper.close(messageDestroyedReason);
             pendingTransportClosed.complete(true);
         } else {
-            mMessageTransportStateTracker.closeGracefully(messageDestroyingReason,
+            mMessageTransportWrapper.closeGracefully(messageDestroyingReason,
                     messageDestroyedReason, pendingTransportClosed::complete);
         }
 
@@ -356,7 +363,7 @@
             DelegateBinderStateManager connection) {
         CompletableFuture<Pair<ISipDelegate, Set<FeatureTagState>>> createdFuture =
                 new CompletableFuture<>();
-        boolean isStarted = connection.create(mMessageTransportStateTracker.getMessageCallback(),
+        boolean isStarted = connection.create(mMessageTransportWrapper.getMessageCallback(),
                 (delegate, delegateDeniedTags) ->
                         createdFuture.complete(new Pair<>(delegate, delegateDeniedTags)));
         if (!isStarted) {
@@ -372,7 +379,7 @@
 
         List<DelegateBinderStateManager.StateCallback> stateCallbacks = new ArrayList<>(2);
         stateCallbacks.add(mDelegateStateTracker);
-        stateCallbacks.add(mMessageTransportStateTracker);
+        stateCallbacks.add(mMessageTransportWrapper);
 
         return mBinderConnectionFactory.create(mSubId,
                 new DelegateRequest(supportedSet), deniedSet, mExecutorService, stateCallbacks);
@@ -400,7 +407,7 @@
         pw.println();
         pw.println("MessageStateTracker:");
         pw.increaseIndent();
-        mMessageTransportStateTracker.dump(pw);
+        mMessageTransportWrapper.dump(pw);
         pw.decreaseIndent();
 
         pw.decreaseIndent();
diff --git a/src/com/android/services/telephony/rcs/TransportSipSessionTracker.java b/src/com/android/services/telephony/rcs/TransportSipSessionTracker.java
new file mode 100644
index 0000000..b90f625
--- /dev/null
+++ b/src/com/android/services/telephony/rcs/TransportSipSessionTracker.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2021 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.SipDelegateConfiguration;
+import android.telephony.ims.SipDelegateImsConfiguration;
+import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.SipMessage;
+import android.util.LocalLog;
+import android.util.Log;
+
+import com.android.services.telephony.rcs.validator.IncomingTransportStateValidator;
+import com.android.services.telephony.rcs.validator.OutgoingTransportStateValidator;
+import com.android.services.telephony.rcs.validator.SipMessageValidator;
+import com.android.services.telephony.rcs.validator.ValidationResult;
+
+import java.io.PrintWriter;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.function.Consumer;
+
+/**
+ * Track incoming and outgoing SIP messages passing through this delegate and verify these messages
+ * by doing the following:
+ *  <ul>
+ *    <li>Track the SipDelegate's registration state to ensure that a registration event has
+ *    occurred before allowing outgoing messages.</li>
+ *    <li>Track the SipDelegate's IMS configuration version and deny any outgoing SipMessages
+ *    associated with a stale IMS configuration version.</li>
+ *    <li>Track the SipDelegate open/close state to allow/deny outgoing messages based on the
+ *    session's state.</li>
+ * </ul>
+ */
+public class TransportSipSessionTracker {
+
+    private static final String LOG_TAG = "SipSessionT";
+
+    private final int mSubId;
+    private final ScheduledExecutorService mExecutor;
+    private final LocalLog mLocalLog = new LocalLog(SipTransportController.LOG_SIZE);
+    // Validators
+    private final IncomingTransportStateValidator mIncomingTransportStateValidator =
+            new IncomingTransportStateValidator();
+    private final OutgoingTransportStateValidator mOutgoingTransportStateValidator =
+            new OutgoingTransportStateValidator();
+    private final SipMessageValidator mOutgoingMessageValidator;
+    private final SipMessageValidator mIncomingMessageValidator;
+
+    private Set<String> mSupportedFeatureTags;
+    private Set<FeatureTagState> mDeniedFeatureTags;
+    private long mConfigVersion = -1;
+    private DelegateRegistrationState mLatestRegistrationState;
+    private Consumer<List<String>> mClosingCompleteConsumer;
+    private Consumer<List<String>> mRegistrationAppliedConsumer;
+
+
+    public TransportSipSessionTracker(int subId, ScheduledExecutorService executor) {
+        mSubId = subId;
+        mExecutor = executor;
+        mOutgoingMessageValidator = mOutgoingTransportStateValidator;
+        mIncomingMessageValidator = mIncomingTransportStateValidator;
+    }
+
+    /**
+     * Notify this tracker that a registration state change has occurred.
+     * <p>
+     * In some scenarios, this will require that existing SIP dialogs are closed (for example, when
+     * moving a feature tag from REGISTERED->DEREGISTERING). This method allows the caller to
+     * provide a Consumer that will be called when either there are no SIP dialogs active on
+     * DEREGISTERING feature tags, or a timeout has occurred. In the case that a timeout has
+     * occurred, this Consumer will accept a list of callIds that will be manually closed by the
+     * framework to unblock the IMS stack.
+     * <p>
+     * @param stateChangeComplete A one time Consumer that when completed, will contain a List of
+     *         callIds corresponding to SIP Dialogs that have not been closed yet. It is the callers
+     *         responsibility to close the dialogs associated with the provided callIds. If another
+     *         state update occurs before the previous was completed, the previous consumer will be
+     *         completed with an empty list and the new Consumer will be executed when the new state
+     *         changes.
+     * @param regState The new registration state.
+     */
+    public void onRegistrationStateChanged(Consumer<List<String>> stateChangeComplete,
+            DelegateRegistrationState regState) {
+        if (mRegistrationAppliedConsumer != null) {
+            logw("onRegistrationStateChanged: pending registration change, completing now.");
+            // complete the pending consumer with no dialogs pending, this will be re-evaluated
+            // and new configuration will be applied.
+            mRegistrationAppliedConsumer.accept(Collections.emptyList());
+        }
+        mLatestRegistrationState = regState;
+        // evaluate if this needs to be set based on reg state.
+        mRegistrationAppliedConsumer = stateChangeComplete;
+        // notify stateChangeComplete when reg state applied
+        mExecutor.execute(() -> {
+            // TODO: Track open regState & signal dialogs to close if required.
+            // Collect open dialogs associated with features that regState is signalling as
+            // DEREGISTERING. When PENDING_DIALOG_CLOSING_TIMEOUT_MS occurs, these dialogs need to
+            // close so that the features can move to DEREGISTERED.
+
+            // For now, just pass back an empty list and complete the Consumer.
+            if (mRegistrationAppliedConsumer != null) {
+                mRegistrationAppliedConsumer.accept(Collections.emptyList());
+                mRegistrationAppliedConsumer = null;
+            }
+        });
+    }
+
+    /**
+     * Notify this tracker that the IMS configuration has changed.
+     *
+     * Parameters contained in the IMS configuration will be used to validate outgoing messages,
+     * such as the configuration version.
+     * @param c The newest IMS configuration.
+     */
+    public void onImsConfigurationChanged(SipDelegateImsConfiguration c) {
+        if (c.getVersion() == mConfigVersion) {
+            return;
+        }
+        logi("onImsConfigurationChanged: " + mConfigVersion + "->" + c.getVersion());
+        mConfigVersion = c.getVersion();
+    }
+
+    /**
+     * Notify this tracker that the IMS configuration has changed.
+     *
+     * Parameters contained in the IMS configuration will be used to validate outgoing messages,
+     * such as the configuration version.
+     * @param c The newest IMS configuration.
+     */
+    public void onConfigurationChanged(SipDelegateConfiguration c) {
+        if (c.getVersion() == mConfigVersion) {
+            return;
+        }
+        logi("onConfigurationChanged: " + mConfigVersion + "->" + c.getVersion());
+        mConfigVersion = c.getVersion();
+    }
+
+    /**
+     * A new message transport has been opened to a SipDelegate.
+     * <p>
+     * Initializes this tracker and resets any state required to process messages.
+     * @param supportedFeatureTags feature tags that are supported and should pass message
+     *                             verification.
+     * @param deniedFeatureTags feature tags that were denied and should fail message verification.
+     */
+    public void onTransportOpened(Set<String> supportedFeatureTags,
+            Set<FeatureTagState> deniedFeatureTags) {
+        logi("onTransportOpened: moving to open state");
+        mSupportedFeatureTags = supportedFeatureTags;
+        mDeniedFeatureTags = deniedFeatureTags;
+        mOutgoingTransportStateValidator.open();
+        mIncomingTransportStateValidator.open();
+    }
+
+    /**
+     * A SIP session has been cleaned up and should no longer be tracked.
+     * @param callId The call ID associated with the SIP session.
+     */
+    public void onSipSessionCleanup(String callId) {
+        //TODO track SIP sessions.
+    }
+
+    /**
+     * Move this tracker into a restricted state, where only outgoing SIP messages associated with
+     * an ongoing SIP Session may be sent. Any out-of-dialog outgoing SIP messages will be denied.
+     * This does not affect incoming SIP messages (for example, an incoming SIP INVITE).
+     * <p>
+     * This tracker will stay in this state until either all open SIP Sessions are closed by the
+     * remote application, or a timeout occurs. Once this happens, the provided Consumer will accept
+     * a List of call IDs associated with the open SIP Sessions that did not close before the
+     * timeout. The caller must then close all open SIP Sessions before closing the transport.
+     * @param closingCompleteConsumer A Consumer that will be called when the transport can be
+     *         closed and may contain a list of callIds associated with SIP sessions that have not
+     *         been closed.
+     * @param closingReason The reason that will be provided if an outgoing out-of-dialog SIP
+     *         message is sent while the transport is closing.
+     * @param closedReason The reason that will be provided if any outgoing SIP message is sent
+     *         once the transport is closed.
+     */
+    public void closeSessionsGracefully(Consumer<List<String>> closingCompleteConsumer,
+            int closingReason, int closedReason) {
+        if (mClosingCompleteConsumer != null) {
+            logw("closeSessionsGracefully: already pending close, completing consumer to unblock");
+            closingCompleteConsumer.accept(Collections.emptyList());
+            return;
+        }
+        logi("closeSessionsGracefully: moving to restricted state, reason=" + closingReason);
+        mClosingCompleteConsumer = closingCompleteConsumer;
+        mOutgoingTransportStateValidator.restrict(closingReason);
+        mExecutor.execute(() -> {
+            logi("closeSessionsGracefully: moving to closed state, reason=" + closedReason);
+            mOutgoingTransportStateValidator.close(closedReason);
+            mIncomingTransportStateValidator.close(closedReason);
+            if (mClosingCompleteConsumer != null) {
+                // TODO: Track SIP sessions and complete when there are no SIP dialogs open anymore
+                //  or the timeout occurs.
+                mClosingCompleteConsumer.accept(Collections.emptyList());
+                mClosingCompleteConsumer = null;
+            }
+        });
+    }
+
+    /**
+     * The message transport must close now due to a configuration change (SIM subscription change,
+     * user disabled RCS, the service is dead, etc...).
+     * @param closedReason The error reason for why the message transport was closed that will be
+     *         sent back to the caller if a new SIP message is sent.
+     * @return A List of call IDs associated with sessions that were still open at the time that the
+     * tracker closed the transport.
+     */
+    public List<String> closeSessionsForcefully(int closedReason) {
+        logi("closeSessionsForcefully: moving to closed state, reason=" + closedReason);
+        mOutgoingTransportStateValidator.close(closedReason);
+        mIncomingTransportStateValidator.close(closedReason);
+        // TODO: add logic to collect open SIP dialogs to be forcefully closed once they are being
+        //  tracked.
+        List<String> openCallIds = Collections.emptyList();
+        if (mClosingCompleteConsumer != null) {
+            logi("closeSessionsForcefully: sending pending call ids through close consumer");
+            // send the call ID through the pending complete mechanism to unblock any previous
+            // graceful close command.
+            mClosingCompleteConsumer.accept(openCallIds);
+            mClosingCompleteConsumer = null;
+            return Collections.emptyList();
+        } else {
+            return openCallIds;
+        }
+    }
+
+    /**
+     * Verify a new outgoing SIP message before sending to the SipDelegate (ImsService).
+     * @param message The SIP message being verified
+     * @return The result of verifying the outgoing message.
+     */
+
+    public ValidationResult verifyOutgoingMessage(SipMessage message, long configVersion) {
+        ValidationResult result = mOutgoingMessageValidator.validate(message);
+        if (!result.isValidated) return result;
+
+        if (mConfigVersion != configVersion) {
+            logi("VerifyOutgoingMessage failed: for message: " + message + ", due to stale IMS "
+                    + "configuration: " + configVersion + ", expected: " + mConfigVersion);
+            return new ValidationResult(
+                    SipDelegateManager.MESSAGE_FAILURE_REASON_STALE_IMS_CONFIGURATION);
+        }
+        if (mLatestRegistrationState == null) {
+            result = new ValidationResult(
+                    SipDelegateManager.MESSAGE_FAILURE_REASON_NOT_REGISTERED);
+        }
+        logi("VerifyOutgoingMessage: " + result + ", message=" + message);
+        return result;
+    }
+
+    /**
+     * Verify a new incoming SIP message before sending it to the
+     * DelegateConnectionMessageCallback (remote application).
+     * @param message The SipMessage to verify.
+     * @return The result of verifying the incoming message.
+     */
+    public ValidationResult verifyIncomingMessage(SipMessage message) {
+        return mIncomingMessageValidator.validate(message);
+    }
+
+    /**
+     * Acknowledge that a pending incoming or outgoing SIP message has been delivered successfully
+     * to the remote.
+     * @param transactionId The transaction ID associated with the message.
+     */
+    public void acknowledgePendingMessage(String transactionId) {
+        logi("acknowledgePendingMessage: id=" + transactionId);
+        //TODO: keep track of pending messages to add to SIP session candidates.
+    }
+
+    /**
+     * A pending incoming or outgoing SIP message has failed and should not be tracked.
+     * @param transactionId
+     */
+    public void notifyPendingMessageFailed(String transactionId) {
+        logi("notifyPendingMessageFailed: id=" + transactionId);
+        //TODO: keep track of pending messages to remove from SIP session candidates.
+    }
+
+    /** Dump state about this tracker that should be included in the dumpsys */
+    public void dump(PrintWriter printWriter) {
+        printWriter.println("Supported Tags:" + mSupportedFeatureTags);
+        printWriter.println("Denied Tags:" + mDeniedFeatureTags);
+        printWriter.println(mOutgoingTransportStateValidator);
+        printWriter.println(mIncomingTransportStateValidator);
+        printWriter.println("Reg consumer pending: " + (mRegistrationAppliedConsumer != null));
+        printWriter.println("Close consumer pending: " + (mClosingCompleteConsumer != null));
+        printWriter.println();
+        printWriter.println("Most recent logs:");
+        mLocalLog.dump(printWriter);
+    }
+
+    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/validator/IncomingTransportStateValidator.java b/src/com/android/services/telephony/rcs/validator/IncomingTransportStateValidator.java
new file mode 100644
index 0000000..dce7841
--- /dev/null
+++ b/src/com/android/services/telephony/rcs/validator/IncomingTransportStateValidator.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2021 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.validator;
+
+import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.SipMessage;
+import android.util.ArrayMap;
+
+/**
+ * Tracks the incoming SIP message transport state from the ImsService to the remote IMS
+ * application. Validates incoming SIP messages based on this state.
+ */
+public final class IncomingTransportStateValidator implements SipMessageValidator {
+
+    /**
+     * The message transport is closed, meaning there can be no more incoming messages
+     */
+    private static final int STATE_CLOSED = 0;
+
+    /**
+     * The message transport is open and incoming traffic is not restricted.
+     */
+    private static final int STATE_OPEN = 1;
+
+    private static final ArrayMap<Integer, String> ENUM_TO_STRING_MAP  = new ArrayMap<>(2);
+    static {
+        ENUM_TO_STRING_MAP.append(STATE_CLOSED, "CLOSED");
+        ENUM_TO_STRING_MAP.append(STATE_OPEN, "OPEN");
+    }
+
+    private int mState = STATE_CLOSED;
+    private int mReason = SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED;
+
+    /**
+     * The SIP message transport is open and will successfully validate SIP messages.
+     */
+    public void open() {
+        mState = STATE_OPEN;
+        mReason = -1;
+    }
+
+    /**
+     * The SIP message transport is closed for incoming SIP messages.
+     * @param reason The error reason sent in response to any incoming SIP messages requests.
+     */
+    public void close(int reason) {
+        mState = STATE_CLOSED;
+        mReason = reason;
+    }
+
+    @Override
+    public ValidationResult validate(SipMessage message) {
+        if (mState != STATE_OPEN) {
+            return new ValidationResult(mReason);
+        }
+        return ValidationResult.SUCCESS;
+    }
+
+    @Override
+    public String toString() {
+        return "Incoming Transport State: " + ENUM_TO_STRING_MAP.getOrDefault(mState,
+                String.valueOf(mState)) + ", reason: "
+                + SipDelegateManager.MESSAGE_FAILURE_REASON_STRING_MAP.getOrDefault(mReason,
+                String.valueOf(mReason));
+    }
+}
diff --git a/src/com/android/services/telephony/rcs/validator/OutgoingTransportStateValidator.java b/src/com/android/services/telephony/rcs/validator/OutgoingTransportStateValidator.java
new file mode 100644
index 0000000..5055e36
--- /dev/null
+++ b/src/com/android/services/telephony/rcs/validator/OutgoingTransportStateValidator.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2021 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.validator;
+
+import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.SipMessage;
+import android.util.ArrayMap;
+
+/**
+ * Tracks the state of the outgoing SIP message transport from the remote IMS application to the
+ * ImsService. Used to validate outgoing SIP messages based off of this state.
+ */
+public final class OutgoingTransportStateValidator implements SipMessageValidator {
+
+    /**
+     * The message transport is closed, meaning there can be no more outgoing messages
+     */
+    private static final int STATE_CLOSED = 0;
+
+    /**
+     * The message transport is restricted to only in-dialog outgoing traffic
+     */
+    private static final int STATE_RESTRICTED = 1;
+
+    /**
+     * The message transport is open and outgoing traffic is not restricted.
+     */
+    private static final int STATE_OPEN = 2;
+
+    private static final ArrayMap<Integer, String> ENUM_TO_STRING_MAP  = new ArrayMap<>(3);
+    static {
+        ENUM_TO_STRING_MAP.append(STATE_CLOSED, "CLOSED");
+        ENUM_TO_STRING_MAP.append(STATE_RESTRICTED, "RESTRICTED");
+        ENUM_TO_STRING_MAP.append(STATE_OPEN, "OPEN");
+    }
+
+    private int mState = STATE_CLOSED;
+    private int mReason = SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED;
+
+    /**
+     * The SIP message transport is open and will successfully validate both in and out of dialog
+     * SIP messages.
+     */
+    public void open() {
+        mState = STATE_OPEN;
+        mReason = -1;
+    }
+
+    /**
+     * The SIP message transport is restricted and only allows in-dialog outgoing messages.
+     * @param reason The reason that will be returned to outgoing out-of-dialog SIP messages that
+     *               are denied.
+     */
+    public void restrict(int reason) {
+        mState = STATE_RESTRICTED;
+        mReason = reason;
+    }
+
+    /**
+     * The SIP message transport is closed for outgoing SIP messages.
+     * @param reason The error reason sent in response to any outgoing SIP messages requests.
+     */
+    public void close(int reason) {
+        mState = STATE_CLOSED;
+        mReason = reason;
+    }
+
+    @Override
+    public ValidationResult validate(SipMessage message) {
+        // TODO: integrate in and out-of-dialog message detection as well as supported & denied tags
+        if (mState != STATE_OPEN) {
+            return new ValidationResult(mReason);
+        }
+        return ValidationResult.SUCCESS;
+    }
+
+    @Override
+    public String toString() {
+        return "Outgoing Transport State: " + ENUM_TO_STRING_MAP.getOrDefault(mState,
+                String.valueOf(mState)) + ", reason: "
+                + SipDelegateManager.MESSAGE_FAILURE_REASON_STRING_MAP.getOrDefault(mReason,
+                String.valueOf(mReason));
+    }
+}
diff --git a/src/com/android/services/telephony/rcs/validator/SipMessageValidator.java b/src/com/android/services/telephony/rcs/validator/SipMessageValidator.java
new file mode 100644
index 0000000..6bbfbf4
--- /dev/null
+++ b/src/com/android/services/telephony/rcs/validator/SipMessageValidator.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2021 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.validator;
+
+import android.telephony.ims.SipMessage;
+
+/**
+ * Validates a SipMessage and returns the result via an instance of {@link ValidationResult}.
+ */
+public interface SipMessageValidator {
+    /**
+     * Validate that the SipMessage is allowed to be sent to the remote.
+     * @param message The SipMessage being validated.
+     * @return A {@link ValidationResult} that represents whether or not the message was validated.
+     * If not validated, it also returns a reason why the SIP message was not validated.
+     */
+    ValidationResult validate(SipMessage message);
+
+    /**
+     * Compose a SipMessageValidator out of two validators, this validator running before the next
+     * validator.
+     * @param next The next validator that will be run if this validator validates the message
+     *             successfully.
+     * @return A new SipMessageValidator composed of this validator and the next one.
+     */
+    default SipMessageValidator andThen(SipMessageValidator next) {
+        return (SipMessage m) -> {
+            ValidationResult result = validate(m);
+            if (!result.isValidated) return result;
+            return next.validate(m);
+        };
+    }
+}
diff --git a/src/com/android/services/telephony/rcs/validator/ValidationResult.java b/src/com/android/services/telephony/rcs/validator/ValidationResult.java
new file mode 100644
index 0000000..f3f6470
--- /dev/null
+++ b/src/com/android/services/telephony/rcs/validator/ValidationResult.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2021 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.validator;
+
+import android.telephony.ims.SipDelegateManager;
+
+/**
+ * Communicates the result of validating whether a SIP message should be sent to a remote based on
+ * the contents of the SIP message as well as if the transport is in an appropriate state for the
+ * intended recipient of the message.
+ */
+public class ValidationResult {
+    public static final ValidationResult SUCCESS = new ValidationResult();
+
+    /**
+     * If {@code true}, the requested SIP message has been validated and may be sent to the remote.
+     * If {@code false}, the SIP message has failed validation and should not be sent to the
+     * remote. The {@link #restrictedReason} field will contain the reason for the validation
+     * failure.
+     */
+    public final boolean isValidated;
+
+    /**
+     * The reason associated with why the SIP message was not verified and generated a
+     * {@code false} result for {@link #isValidated}.
+     */
+    public final int restrictedReason;
+
+    /**
+     * Communicates a validated result of success. Use {@link #SUCCESS} instead.
+     */
+    private ValidationResult() {
+        isValidated = true;
+        restrictedReason = SipDelegateManager.MESSAGE_FAILURE_REASON_UNKNOWN;
+    }
+
+    /**
+     * The result of validating that the SIP Message should be sent.
+     *
+     * @param reason The reason associated with why the SIP message was not validated and
+     *               generated a {@code false} result for {@link #isValidated}.
+     */
+    public ValidationResult(@SipDelegateManager.MessageFailureReason int reason) {
+        isValidated = false;
+        restrictedReason = reason;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder b = new StringBuilder();
+        b.append("ValidationResult{");
+        b.append("validated=");
+        b.append(isValidated);
+        if (!isValidated) {
+            b.append(", restrictedReason=");
+            b.append(restrictedReason);
+        }
+        b.append('}');
+        return b.toString();
+    }
+}
diff --git a/tests/src/com/android/TestExecutorService.java b/tests/src/com/android/TestExecutorService.java
index fec502a..7685c6d 100644
--- a/tests/src/com/android/TestExecutorService.java
+++ b/tests/src/com/android/TestExecutorService.java
@@ -16,9 +16,11 @@
 
 package com.android;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Delayed;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
@@ -37,6 +39,8 @@
 
         private final Callable<T> mTask;
         private final long mDelayMs;
+        // Wrap callable in a CompletableFuture to support delays in execution.
+        private final CompletableFuture<T> mFuture = new CompletableFuture<>();
 
         CompletedFuture(Callable<T> task) {
             mTask = task;
@@ -50,36 +54,29 @@
 
         @Override
         public boolean cancel(boolean mayInterruptIfRunning) {
-            return false;
+            return mFuture.cancel(mayInterruptIfRunning);
         }
 
         @Override
         public boolean isCancelled() {
-            return false;
+            return mFuture.isCancelled();
         }
 
         @Override
         public boolean isDone() {
-            return true;
+            return mFuture.isDone();
         }
 
         @Override
         public T get() throws InterruptedException, ExecutionException {
-            try {
-                return mTask.call();
-            } catch (Exception e) {
-                throw new ExecutionException(e);
-            }
+            return mFuture.get();
         }
 
         @Override
         public T get(long timeout, TimeUnit unit)
                 throws InterruptedException, ExecutionException, TimeoutException {
-            try {
-                return mTask.call();
-            } catch (Exception e) {
-                throw new ExecutionException(e);
-            }
+            // delays not implemented, this should complete via completeTask for better control.
+            return mFuture.get(timeout, unit);
         }
 
         @Override
@@ -99,35 +96,71 @@
             if (o.getDelay(TimeUnit.MILLISECONDS) < mDelayMs) return 1;
             return 0;
         }
+
+        public void completeTask() {
+            try {
+                mFuture.complete(mTask.call());
+            } catch (Exception e) {
+                mFuture.completeExceptionally(e);
+            }
+        }
+    }
+
+    private final ArrayList<Runnable> mPendingRunnables = new ArrayList<>();
+    private final boolean mWaitToComplete;
+    private boolean mIsShutdown = false;
+
+    public TestExecutorService() {
+        mWaitToComplete = false;
+    }
+
+    /**
+     * Create a test executor service that also allows the constructor to provide a parameter to
+     * control when pending Runnables are executed.
+     * @param waitToComplete If true, this executor will wait to complete any pending Runnables
+     *                       until {@link #executePending()}} is called.
+     */
+    public TestExecutorService(boolean waitToComplete) {
+        mWaitToComplete = waitToComplete;
     }
 
     @Override
     public void shutdown() {
+        mIsShutdown = true;
+        for (Runnable r : mPendingRunnables) {
+            r.run();
+        }
     }
 
     @Override
     public List<Runnable> shutdownNow() {
-        return null;
+        mIsShutdown = true;
+        List<Runnable> runnables = new ArrayList<>(mPendingRunnables);
+        mPendingRunnables.clear();
+        return runnables;
     }
 
     @Override
     public boolean isShutdown() {
-        return false;
+        return mIsShutdown;
     }
 
     @Override
     public boolean isTerminated() {
-        return false;
+        return mIsShutdown;
     }
 
     @Override
     public boolean awaitTermination(long timeout, TimeUnit unit) {
-        return false;
+        shutdown();
+        return true;
     }
 
     @Override
     public <T> Future<T> submit(Callable<T> task) {
-        return new CompletedFuture<>(task);
+        CompletedFuture<T> f = new CompletedFuture<>(task);
+        onExecute(f::completeTask);
+        return f;
     }
 
     @Override
@@ -137,8 +170,12 @@
 
     @Override
     public Future<?> submit(Runnable task) {
-        task.run();
-        return new CompletedFuture<>(() -> null);
+        CompletedFuture<Void> f = new CompletedFuture<>(() -> {
+            task.run();
+            return null;
+        });
+        onExecute(f::completeTask);
+        return f;
     }
 
     @Override
@@ -164,14 +201,21 @@
 
     @Override
     public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
-        // No need to worry about delays yet
-        command.run();
-        return new CompletedFuture<>(() -> null, delay);
+        long millisDelay = TimeUnit.MILLISECONDS.convert(delay, unit);
+        CompletedFuture<Void> f = new CompletedFuture<>(() -> {
+            command.run();
+            return null;
+        }, millisDelay);
+        onExecute(f::completeTask);
+        return f;
     }
 
     @Override
     public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {
-        return new CompletedFuture<>(callable, delay);
+        long millisDelay = TimeUnit.MILLISECONDS.convert(delay, unit);
+        CompletedFuture<V> f = new CompletedFuture<>(callable, millisDelay);
+        onExecute(f::completeTask);
+        return f;
     }
 
     @Override
@@ -188,6 +232,21 @@
 
     @Override
     public void execute(Runnable command) {
-        command.run();
+        onExecute(command);
+    }
+
+    private void onExecute(Runnable command) {
+        if (mWaitToComplete) {
+            mPendingRunnables.add(command);
+        } else {
+            command.run();
+        }
+    }
+
+    public void executePending() {
+        for (Runnable r : mPendingRunnables) {
+            r.run();
+        }
+        mPendingRunnables.clear();
     }
 }
diff --git a/tests/src/com/android/services/telephony/rcs/MessageTransportStateTrackerTest.java b/tests/src/com/android/services/telephony/rcs/MessageTransportStateTrackerTest.java
deleted file mode 100644
index f69b9a8..0000000
--- a/tests/src/com/android/services/telephony/rcs/MessageTransportStateTrackerTest.java
+++ /dev/null
@@ -1,243 +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.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 testDelegateConnectionCloseSession() throws Exception {
-        MessageTransportStateTracker tracker = new MessageTransportStateTracker(TEST_SUB_ID,
-                Runnable::run, mDelegateMessageCallback);
-        tracker.openTransport(mISipDelegate, Collections.emptySet());
-        tracker.getDelegateConnection().cleanupSession("testCallId");
-        verify(mISipDelegate).cleanupSession("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/MessageTransportWrapperTest.java b/tests/src/com/android/services/telephony/rcs/MessageTransportWrapperTest.java
new file mode 100644
index 0000000..f273854
--- /dev/null
+++ b/tests/src/com/android/services/telephony/rcs/MessageTransportWrapperTest.java
@@ -0,0 +1,343 @@
+/*
+ * 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 junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+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.net.InetAddresses;
+import android.os.RemoteException;
+import android.telephony.ims.DelegateRegistrationState;
+import android.telephony.ims.FeatureTagState;
+import android.telephony.ims.SipDelegateConfiguration;
+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 com.android.TestExecutorService;
+import com.android.services.telephony.rcs.validator.ValidationResult;
+
+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.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.function.Consumer;
+
+@RunWith(AndroidJUnit4.class)
+public class MessageTransportWrapperTest 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]);
+
+    // Derived from TEST_MESSAGE above.
+    private static final String TEST_TRANSACTION_ID = "z9hG4bK776asdhds";
+
+    @Mock private ISipDelegateMessageCallback mDelegateMessageCallback;
+    @Mock private TransportSipSessionTracker mTransportSipSessionTracker;
+    @Mock private ISipDelegate mISipDelegate;
+    @Mock private Consumer<Boolean> mMockCloseConsumer;
+
+    // Test executor that just calls run on the Runnable provided in execute.
+    private ScheduledExecutorService mExecutor = new TestExecutorService();
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @SmallTest
+    @Test
+    public void testImsConfigurationChanged() {
+        MessageTransportWrapper tracker = createTestMessageTransportWrapper();
+        InetSocketAddress localAddr = new InetSocketAddress(
+                InetAddresses.parseNumericAddress("1.1.1.1"), 80);
+        InetSocketAddress serverAddr = new InetSocketAddress(
+                InetAddresses.parseNumericAddress("2.2.2.2"), 81);
+        SipDelegateConfiguration c = new SipDelegateConfiguration.Builder(1,
+                SipDelegateConfiguration.SIP_TRANSPORT_TCP, localAddr, serverAddr).build();
+        // Ensure IMS config changes are propagated to the message tracker.
+        tracker.onConfigurationChanged(c);
+        verify(mTransportSipSessionTracker).onConfigurationChanged(c);
+    }
+
+    @SmallTest
+    @Test
+    public void testOpenTransport() {
+        HashSet<String> allowedTags = new HashSet<>(1);
+        allowedTags.add("testTag");
+        HashSet<FeatureTagState> deniedTags = new HashSet<>(1);
+        deniedTags.add(new FeatureTagState("testBadTag",
+                SipDelegateManager.DENIED_REASON_INVALID));
+        MessageTransportWrapper tracker = createTestMessageTransportWrapper();
+        // Ensure openTransport passes denied tags to the session tracker
+        tracker.openTransport(mISipDelegate, allowedTags, deniedTags);
+        verify(mTransportSipSessionTracker).onTransportOpened(allowedTags, deniedTags);
+    }
+
+    @SmallTest
+    @Test
+    public void testRegistrationStateChanged() throws Exception {
+        ArrayList<String> callIds = new ArrayList<>(2);
+        callIds.add("callId1");
+        callIds.add("callId2");
+        // empty registration state for testing
+        DelegateRegistrationState state = new DelegateRegistrationState.Builder().build();
+        MessageTransportWrapper tracker = createTestMessageTransportWrapper();
+        tracker.openTransport(mISipDelegate, Collections.emptySet(), Collections.emptySet());
+
+        Consumer<List<String>> callIdConsumer = trackerRegStateChanged(tracker, state);
+        callIdConsumer.accept(callIds);
+        // Verify that the pending call IDs are closed properly.
+        for (String callId : callIds) {
+            verify(mTransportSipSessionTracker).onSipSessionCleanup(callId);
+            verify(mISipDelegate).cleanupSession(callId);
+        }
+    }
+
+    @SmallTest
+    @Test
+    public void testCloseGracefully() throws Exception {
+        int closingReason = DelegateRegistrationState.DEREGISTERING_REASON_PROVISIONING_CHANGE;
+        int closedReason = DelegateRegistrationState.DEREGISTERED_REASON_NOT_PROVISIONED;
+        MessageTransportWrapper tracker = createTestMessageTransportWrapper();
+
+        Boolean[] result = new Boolean[1];
+        Consumer<List<String>> callIdConsumer = closeTrackerGracefully(tracker, closingReason,
+                closedReason, (r) -> result[0] = r);
+        callIdConsumer.accept(Collections.emptyList());
+        // Verify that the pending call IDs are closed properly.
+        verify(mTransportSipSessionTracker, never()).onSipSessionCleanup(anyString());
+        verify(mISipDelegate, never()).cleanupSession(anyString());
+        // Result is true in the case that all call IDs were successfully closed.
+        assertTrue(result[0]);
+    }
+
+    @SmallTest
+    @Test
+    public void testCloseGracefullyForceCloseCallIds() throws Exception {
+        ArrayList<String> callIds = new ArrayList<>(2);
+        callIds.add("callId1");
+        callIds.add("callId2");
+        int closingReason = DelegateRegistrationState.DEREGISTERING_REASON_PROVISIONING_CHANGE;
+        int closedReason = DelegateRegistrationState.DEREGISTERED_REASON_NOT_PROVISIONED;
+        MessageTransportWrapper tracker = createTestMessageTransportWrapper();
+        tracker.openTransport(mISipDelegate, Collections.emptySet(), Collections.emptySet());
+
+        Boolean[] result = new Boolean[1];
+        Consumer<List<String>> callIdConsumer = closeTrackerGracefully(tracker, closingReason,
+                closedReason, (r) -> result[0] = r);
+        callIdConsumer.accept(callIds);
+        // Verify that the pending call IDs are closed properly.
+        for (String callId : callIds) {
+            verify(mTransportSipSessionTracker).onSipSessionCleanup(callId);
+            verify(mISipDelegate).cleanupSession(callId);
+        }
+        // Result is false in this case because there were still callIds left that were not
+        // successfully closed.
+        assertFalse(result[0]);
+    }
+
+    @SmallTest
+    @Test
+    public void testClose() throws Exception {
+        ArrayList<String> callIds = new ArrayList<>(2);
+        callIds.add("callId1");
+        callIds.add("callId2");
+        int closedReason = SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED;
+        doReturn(callIds).when(mTransportSipSessionTracker).closeSessionsForcefully(closedReason);
+        MessageTransportWrapper tracker = createTestMessageTransportWrapper();
+        tracker.openTransport(mISipDelegate, Collections.emptySet(), Collections.emptySet());
+
+        tracker.close(closedReason);
+        // Verify that the pending call IDs are closed properly.
+        for (String callId : callIds) {
+            verify(mTransportSipSessionTracker).onSipSessionCleanup(callId);
+            verify(mISipDelegate).cleanupSession(callId);
+        }
+    }
+
+    @SmallTest
+    @Test
+    public void testDelegateConnectionSendOutgoingMessage() throws Exception {
+        MessageTransportWrapper tracker = createTestMessageTransportWrapper();
+
+        tracker.openTransport(mISipDelegate, Collections.emptySet(), Collections.emptySet());
+        doReturn(ValidationResult.SUCCESS)
+                .when(mTransportSipSessionTracker)
+                .verifyOutgoingMessage(TEST_MESSAGE, 1 /*version*/);
+        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));
+
+        doReturn(new ValidationResult(
+                SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED))
+                .when(mTransportSipSessionTracker)
+                .verifyOutgoingMessage(TEST_MESSAGE, 1 /*version*/);
+        tracker.getDelegateConnection().sendMessage(TEST_MESSAGE, 1 /*version*/);
+        verify(mDelegateMessageCallback).onMessageSendFailure(TEST_TRANSACTION_ID,
+                SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED);
+    }
+
+    @SmallTest
+    @Test
+    public void testDelegateConnectionNotifyMessageReceived() throws Exception {
+        MessageTransportWrapper tracker = createTestMessageTransportWrapper();
+        tracker.openTransport(mISipDelegate, Collections.emptySet(), Collections.emptySet());
+        tracker.getDelegateConnection().notifyMessageReceived(TEST_TRANSACTION_ID);
+        verify(mISipDelegate).notifyMessageReceived(TEST_TRANSACTION_ID);
+        verify(mTransportSipSessionTracker).acknowledgePendingMessage(TEST_TRANSACTION_ID);
+    }
+
+    @SmallTest
+    @Test
+    public void testDelegateConnectionNotifyMessageReceiveError() throws Exception {
+        MessageTransportWrapper tracker = createTestMessageTransportWrapper();
+        tracker.openTransport(mISipDelegate, Collections.emptySet(), Collections.emptySet());
+        tracker.getDelegateConnection().notifyMessageReceiveError(TEST_TRANSACTION_ID,
+                SipDelegateManager.MESSAGE_FAILURE_REASON_NETWORK_NOT_AVAILABLE);
+        verify(mISipDelegate).notifyMessageReceiveError(TEST_TRANSACTION_ID,
+                SipDelegateManager.MESSAGE_FAILURE_REASON_NETWORK_NOT_AVAILABLE);
+        verify(mTransportSipSessionTracker).notifyPendingMessageFailed(TEST_TRANSACTION_ID);
+    }
+
+    @SmallTest
+    @Test
+    public void testDelegateConnectionCloseSession() throws Exception {
+        MessageTransportWrapper tracker = createTestMessageTransportWrapper();
+        tracker.openTransport(mISipDelegate, Collections.emptySet(), Collections.emptySet());
+        tracker.getDelegateConnection().cleanupSession("testCallId");
+        verify(mISipDelegate).cleanupSession("testCallId");
+        verify(mTransportSipSessionTracker).onSipSessionCleanup("testCallId");
+    }
+
+    @SmallTest
+    @Test
+    public void testDelegateOnMessageReceived() throws Exception {
+        MessageTransportWrapper tracker = createTestMessageTransportWrapper();
+        tracker.openTransport(mISipDelegate, Collections.emptySet(), Collections.emptySet());
+
+        doReturn(ValidationResult.SUCCESS)
+                .when(mTransportSipSessionTracker).verifyIncomingMessage(TEST_MESSAGE);
+        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(TEST_TRANSACTION_ID,
+                SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_DEAD);
+
+        doReturn(new ValidationResult(
+                SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_DEAD))
+                .when(mTransportSipSessionTracker).verifyIncomingMessage(TEST_MESSAGE);
+        tracker.getMessageCallback().onMessageReceived(TEST_MESSAGE);
+        verify(mISipDelegate, times(2)).notifyMessageReceiveError(TEST_TRANSACTION_ID,
+                SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_DEAD);
+    }
+
+    @SmallTest
+    @Test
+    public void testDelegateOnMessageSent() throws Exception {
+        MessageTransportWrapper tracker = createTestMessageTransportWrapper();
+        tracker.openTransport(mISipDelegate, Collections.emptySet(), Collections.emptySet());
+        tracker.getMessageCallback().onMessageSent(TEST_TRANSACTION_ID);
+        verify(mTransportSipSessionTracker).acknowledgePendingMessage(TEST_TRANSACTION_ID);
+        verify(mDelegateMessageCallback).onMessageSent(TEST_TRANSACTION_ID);
+    }
+
+    @SmallTest
+    @Test
+    public void testDelegateOnMessageSendFailure() throws Exception {
+        MessageTransportWrapper tracker = createTestMessageTransportWrapper();
+        tracker.openTransport(mISipDelegate, Collections.emptySet(), Collections.emptySet());
+        tracker.getMessageCallback().onMessageSendFailure(TEST_TRANSACTION_ID,
+                SipDelegateManager.MESSAGE_FAILURE_REASON_NETWORK_NOT_AVAILABLE);
+        verify(mTransportSipSessionTracker).notifyPendingMessageFailed(TEST_TRANSACTION_ID);
+        verify(mDelegateMessageCallback).onMessageSendFailure(TEST_TRANSACTION_ID,
+                SipDelegateManager.MESSAGE_FAILURE_REASON_NETWORK_NOT_AVAILABLE);
+    }
+
+    private MessageTransportWrapper createTestMessageTransportWrapper() {
+        return new MessageTransportWrapper(TEST_SUB_ID,
+                mExecutor, mDelegateMessageCallback, mTransportSipSessionTracker);
+    }
+
+    private Consumer<List<String>> trackerRegStateChanged(MessageTransportWrapper tracker,
+            DelegateRegistrationState state) {
+        ArrayList<Consumer<List<String>>> consumerCaptor = new ArrayList<>(1);
+        Mockito.doAnswer(it -> {
+            // Capture the consumer here.
+            consumerCaptor.add(it.getArgument(0));
+            return null;
+        }).when(mTransportSipSessionTracker).onRegistrationStateChanged(any(), eq(state));
+        tracker.onRegistrationStateChanged(state);
+        verify(mTransportSipSessionTracker).onRegistrationStateChanged(any(), eq(state));
+        assertFalse(consumerCaptor.isEmpty());
+        return consumerCaptor.get(0);
+    }
+
+    private Consumer<List<String>> closeTrackerGracefully(MessageTransportWrapper tracker,
+            int closingReason, int closedReason, Consumer<Boolean> resultConsumer) {
+        ArrayList<Consumer<List<String>>> consumerCaptor = new ArrayList<>(1);
+        Mockito.doAnswer(it -> {
+            // Capture the consumer here.
+            consumerCaptor.add(it.getArgument(0));
+            return null;
+        }).when(mTransportSipSessionTracker).closeSessionsGracefully(any(), eq(closingReason),
+                eq(closedReason));
+        tracker.closeGracefully(closingReason, closedReason, resultConsumer);
+        verify(mTransportSipSessionTracker).closeSessionsGracefully(any(), eq(closingReason),
+                eq(closedReason));
+        assertFalse(consumerCaptor.isEmpty());
+        return consumerCaptor.get(0);
+    }
+}
diff --git a/tests/src/com/android/services/telephony/rcs/SipDelegateControllerTest.java b/tests/src/com/android/services/telephony/rcs/SipDelegateControllerTest.java
index 27f896b..5b0e7c5 100644
--- a/tests/src/com/android/services/telephony/rcs/SipDelegateControllerTest.java
+++ b/tests/src/com/android/services/telephony/rcs/SipDelegateControllerTest.java
@@ -58,13 +58,14 @@
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
+import java.util.stream.Collectors;
 
 @RunWith(AndroidJUnit4.class)
 public class SipDelegateControllerTest extends TelephonyTestBase {
     private static final int TEST_SUB_ID = 1;
 
     @Mock private ISipDelegate mMockSipDelegate;
-    @Mock private MessageTransportStateTracker mMockMessageTracker;
+    @Mock private MessageTransportWrapper mMockMessageTracker;
     @Mock private ISipDelegateMessageCallback mMockMessageCallback;
     @Mock private DelegateStateTracker mMockDelegateStateTracker;
     @Mock private DelegateBinderStateManager mMockBinderConnection;
@@ -104,12 +105,44 @@
         assertFalse(future.isDone());
         consumer.accept(mMockSipDelegate, Collections.emptySet());
         assertTrue(future.get());
-        verify(mMockMessageTracker).openTransport(mMockSipDelegate, Collections.emptySet());
+        verify(mMockMessageTracker).openTransport(mMockSipDelegate, request.getFeatureTags(),
+                Collections.emptySet());
         verify(mMockDelegateStateTracker).sipDelegateConnected(Collections.emptySet());
     }
 
     @SmallTest
     @Test
+    public void testCreateDeniedFeatures() throws Exception {
+        DelegateRequest request = getLargeDelegateRequest();
+        ArraySet<FeatureTagState> deniedTags = new ArraySet<>(1);
+        deniedTags.add(new FeatureTagState(ImsSignallingUtils.GROUP_CHAT_TAG,
+                SipDelegateManager.DENIED_REASON_NOT_ALLOWED));
+        SipDelegateController controller = getTestDelegateController(request,
+                deniedTags);
+
+        doReturn(true).when(mMockBinderConnection).create(eq(mMockMessageCallback), any());
+        CompletableFuture<Boolean> future = controller.create(request.getFeatureTags(),
+                deniedTags);
+        BiConsumer<ISipDelegate, Set<FeatureTagState>> consumer =
+                verifyConnectionCreated(1);
+        assertNotNull(consumer);
+
+        assertFalse(future.isDone());
+        // Send in additional tags denied by the service
+        deniedTags.add(new FeatureTagState(ImsSignallingUtils.FILE_TRANSFER_HTTP_TAG,
+                SipDelegateManager.DENIED_REASON_NOT_ALLOWED));
+        consumer.accept(mMockSipDelegate, deniedTags);
+        assertTrue(future.get());
+        // Allowed tags should be initial request set - denied tags
+        ArraySet<String> allowedTags = new ArraySet<>(request.getFeatureTags());
+        allowedTags.removeAll(deniedTags.stream().map(FeatureTagState::getFeatureTag)
+                .collect(Collectors.toSet()));
+        verify(mMockMessageTracker).openTransport(mMockSipDelegate, allowedTags, deniedTags);
+        verify(mMockDelegateStateTracker).sipDelegateConnected(deniedTags);
+    }
+
+    @SmallTest
+    @Test
     public void testCreateDelegateTransportDied() throws Exception {
         DelegateRequest request = getBaseDelegateRequest();
         SipDelegateController controller = getTestDelegateController(request,
@@ -212,7 +245,9 @@
         consumer.accept(mMockSipDelegate, Collections.emptySet());
         assertTrue(pendingChange.get());
 
-        verify(mMockMessageTracker, times(2)).openTransport(mMockSipDelegate,
+        verify(mMockMessageTracker).openTransport(mMockSipDelegate, request.getFeatureTags(),
+                Collections.emptySet());
+        verify(mMockMessageTracker).openTransport(mMockSipDelegate, newFts,
                 Collections.emptySet());
         verify(mMockDelegateStateTracker, times(2)).sipDelegateConnected(Collections.emptySet());
     }
@@ -235,10 +270,22 @@
         return request;
     }
 
+    private ArraySet<String> getLargeFTSet() {
+        ArraySet<String> request = new ArraySet<>();
+        request.add(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG);
+        request.add(ImsSignallingUtils.GROUP_CHAT_TAG);
+        request.add(ImsSignallingUtils.FILE_TRANSFER_HTTP_TAG);
+        return request;
+    }
+
     private DelegateRequest getBaseDelegateRequest() {
         return new DelegateRequest(getBaseFTSet());
     }
 
+    private DelegateRequest getLargeDelegateRequest() {
+        return new DelegateRequest(getLargeFTSet());
+    }
+
     private SipDelegateController getTestDelegateController(DelegateRequest request,
             Set<FeatureTagState> deniedSet) {
         return new SipDelegateController(TEST_SUB_ID, request, "", mExecutorService,
diff --git a/tests/src/com/android/services/telephony/rcs/TransportSipSessionTrackerTest.java b/tests/src/com/android/services/telephony/rcs/TransportSipSessionTrackerTest.java
new file mode 100644
index 0000000..e076605
--- /dev/null
+++ b/tests/src/com/android/services/telephony/rcs/TransportSipSessionTrackerTest.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2021 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 junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.net.InetAddresses;
+import android.telephony.ims.DelegateRegistrationState;
+import android.telephony.ims.SipDelegateConfiguration;
+import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.SipMessage;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.TelephonyTestBase;
+import com.android.TestExecutorService;
+import com.android.services.telephony.rcs.validator.ValidationResult;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InetSocketAddress;
+import java.util.Collections;
+import java.util.concurrent.ScheduledExecutorService;
+
+@RunWith(AndroidJUnit4.class)
+public class TransportSipSessionTrackerTest extends TelephonyTestBase {
+
+    private static final int TEST_SUB_ID = 1;
+    private static final int TEST_CONFIG_VERSION = 1;
+    private static final SipMessage TEST_MESSAGE = new SipMessage(
+            "INVITE sip:bob@biloxi.com SIP/2.0",
+            // Typical Via
+            "Via: SIP/2.0/UDP pc33.atlanta.com;branch=z9hG4bK776asdhds\n"
+                    + "Max-Forwards: 70\n"
+                    + "To: Bob <sip:bob@biloxi.com>\n"
+                    + "From: Alice <sip:alice@atlanta.com>;tag=1928301774\n"
+                    + "Call-ID: a84b4c76e66710@pc33.atlanta.com\n"
+                    + "CSeq: 314159 INVITE\n"
+                    + "Contact: <sip:alice@pc33.atlanta.com>\n"
+                    + "Content-Type: application/sdp\n"
+                    + "Content-Length: 142",
+            new byte[0]);
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    public void testTransportOpening() {
+        TestExecutorService executor = new TestExecutorService();
+        TransportSipSessionTracker tracker = getTestTracker(executor);
+        // Before the transport is opened, incoming/outgoing messages must fail.
+        assertFalse(isIncomingTransportOpen(tracker));
+        assertFalse(isOutgoingTransportOpen(tracker));
+        tracker.onTransportOpened(Collections.emptySet(), Collections.emptySet());
+        // Incoming messages are already verified
+        assertTrue(isIncomingTransportOpen(tracker));
+        // IMS config and registration state needs to be sent before outgoing messages can be
+        // verified.
+        assertFalse(isOutgoingTransportOpen(tracker));
+        tracker.onConfigurationChanged(getConfigBuilder(TEST_CONFIG_VERSION).build());
+        // Incoming messages are already verified
+        assertTrue(isIncomingTransportOpen(tracker));
+        assertFalse(isOutgoingTransportOpen(tracker));
+        tracker.onRegistrationStateChanged((ignore) -> {}, getTestRegistrationState());
+        // Config set + IMS reg state sent, transport is now open.
+        assertTrue(isIncomingTransportOpen(tracker));
+        assertTrue(isOutgoingTransportOpen(tracker));
+    }
+
+    @Test
+    public void testTransportOpenConfigChange() {
+        TestExecutorService executor = new TestExecutorService();
+        TransportSipSessionTracker tracker = getTestTracker(executor);
+        tracker.onTransportOpened(Collections.emptySet(), Collections.emptySet());
+        tracker.onConfigurationChanged(getConfigBuilder(TEST_CONFIG_VERSION).build());
+        tracker.onRegistrationStateChanged((ignore) -> {}, getTestRegistrationState());
+        // Config set + IMS reg state sent, transport is now open.
+        assertTrue(isIncomingTransportOpen(tracker));
+        assertTrue(isOutgoingTransportOpen(tracker));
+
+        // Update IMS config version and send a message with an outdated version.
+        tracker.onConfigurationChanged(getConfigBuilder(TEST_CONFIG_VERSION + 1).build());
+        assertEquals(SipDelegateManager.MESSAGE_FAILURE_REASON_STALE_IMS_CONFIGURATION,
+                verifyOutgoingTransportClosed(tracker));
+    }
+
+    @Test
+    public void testTransportClosingGracefully() {
+        TestExecutorService executor = new TestExecutorService(true /*wait*/);
+        TransportSipSessionTracker tracker = getTestTracker(executor);
+        tracker.onTransportOpened(Collections.emptySet(), Collections.emptySet());
+        tracker.onConfigurationChanged(getConfigBuilder(TEST_CONFIG_VERSION).build());
+        tracker.onRegistrationStateChanged((ignore) -> {}, getTestRegistrationState());
+        // Config set + IMS reg state sent, transport is now open.
+        assertTrue(isIncomingTransportOpen(tracker));
+        assertTrue(isOutgoingTransportOpen(tracker));
+
+        tracker.closeSessionsGracefully((ignore) -> {},
+                SipDelegateManager.MESSAGE_FAILURE_REASON_INTERNAL_DELEGATE_STATE_TRANSITION,
+                SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED);
+
+        // Before executor executes, outgoing messages will be restricted.
+        assertTrue(isIncomingTransportOpen(tracker));
+        assertEquals(SipDelegateManager.MESSAGE_FAILURE_REASON_INTERNAL_DELEGATE_STATE_TRANSITION,
+                verifyOutgoingTransportClosed(tracker));
+        executor.executePending();
+        // After Executor executes, all messages will be rejected.
+        assertEquals(SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED,
+                verifyOutgoingTransportClosed(tracker));
+        assertEquals(SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED,
+                verifyIncomingTransportClosed(tracker));
+    }
+
+    @Test
+    public void testTransportClosingForcefully() {
+        TestExecutorService executor = new TestExecutorService();
+        TransportSipSessionTracker tracker = getTestTracker(executor);
+        tracker.onTransportOpened(Collections.emptySet(), Collections.emptySet());
+        tracker.onConfigurationChanged(getConfigBuilder(TEST_CONFIG_VERSION).build());
+        tracker.onRegistrationStateChanged((ignore) -> {}, getTestRegistrationState());
+        // Config set + IMS reg state sent, transport is now open.
+        assertTrue(isIncomingTransportOpen(tracker));
+        assertTrue(isOutgoingTransportOpen(tracker));
+
+        tracker.closeSessionsForcefully(
+                SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED);
+
+        // All messages will be rejected.
+        assertEquals(SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED,
+                verifyOutgoingTransportClosed(tracker));
+        assertEquals(SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED,
+                verifyIncomingTransportClosed(tracker));
+    }
+
+    private SipDelegateConfiguration.Builder getConfigBuilder(int version) {
+        InetSocketAddress localAddr = new InetSocketAddress(
+                InetAddresses.parseNumericAddress("1.1.1.1"), 80);
+        InetSocketAddress serverAddr = new InetSocketAddress(
+                InetAddresses.parseNumericAddress("2.2.2.2"), 81);
+        return new SipDelegateConfiguration.Builder(version,
+                SipDelegateConfiguration.SIP_TRANSPORT_TCP, localAddr, serverAddr);
+    }
+
+    private boolean isIncomingTransportOpen(TransportSipSessionTracker tracker) {
+        return tracker.verifyIncomingMessage(TEST_MESSAGE).isValidated;
+    }
+
+    private boolean isOutgoingTransportOpen(TransportSipSessionTracker tracker) {
+        return tracker.verifyOutgoingMessage(TEST_MESSAGE, TEST_CONFIG_VERSION).isValidated;
+    }
+
+    private int verifyIncomingTransportClosed(TransportSipSessionTracker tracker) {
+        ValidationResult result = tracker.verifyIncomingMessage(TEST_MESSAGE);
+        assertFalse(result.isValidated);
+        return result.restrictedReason;
+    }
+
+    private int verifyOutgoingTransportClosed(TransportSipSessionTracker tracker) {
+        ValidationResult result = tracker.verifyOutgoingMessage(TEST_MESSAGE, TEST_CONFIG_VERSION);
+        assertFalse(result.isValidated);
+        return result.restrictedReason;
+    }
+
+    private DelegateRegistrationState getTestRegistrationState() {
+        return new DelegateRegistrationState.Builder().build();
+    }
+
+    private TransportSipSessionTracker getTestTracker(ScheduledExecutorService executor) {
+        return new TransportSipSessionTracker(TEST_SUB_ID, executor);
+    }
+}
diff --git a/tests/src/com/android/services/telephony/rcs/validator/IncomingTransportStateValidatorTest.java b/tests/src/com/android/services/telephony/rcs/validator/IncomingTransportStateValidatorTest.java
new file mode 100644
index 0000000..37cd462
--- /dev/null
+++ b/tests/src/com/android/services/telephony/rcs/validator/IncomingTransportStateValidatorTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2021 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.validator;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.SipMessage;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class IncomingTransportStateValidatorTest {
+    private static final SipMessage TEST_MESSAGE = new SipMessage(
+            "INVITE sip:bob@biloxi.com SIP/2.0",
+            "Via: SIP/2.0/UDP pc33.atlanta.com;branch=z9hG4bK776asdhds\n"
+                    + "Max-Forwards: 70\n"
+                    + "To: Bob <sip:bob@biloxi.com>\n"
+                    + "From: Alice <sip:alice@atlanta.com>;tag=1928301774\n"
+                    + "Call-ID: a84b4c76e66710@pc33.atlanta.com\n"
+                    + "CSeq: 314159 INVITE\n"
+                    + "Contact: <sip:alice@pc33.atlanta.com>\n"
+                    + "Content-Type: application/sdp\n"
+                    + "Content-Length: 142",
+            new byte[0]);
+
+    @Test
+    public void testVerifyMessageAndUpdateState() {
+        IncomingTransportStateValidator validator = new IncomingTransportStateValidator();
+        ValidationResult result = validator.validate(TEST_MESSAGE);
+        assertFalse(result.isValidated);
+        assertEquals(SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED,
+                result.restrictedReason);
+
+        validator.open();
+        result = validator.validate(TEST_MESSAGE);
+        assertTrue(result.isValidated);
+
+        validator.close(SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED);
+        result = validator.validate(TEST_MESSAGE);
+        assertFalse(result.isValidated);
+        assertEquals(SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED,
+                result.restrictedReason);
+    }
+}
diff --git a/tests/src/com/android/services/telephony/rcs/validator/OutgoingTransportStateValidatorTest.java b/tests/src/com/android/services/telephony/rcs/validator/OutgoingTransportStateValidatorTest.java
new file mode 100644
index 0000000..ae20688
--- /dev/null
+++ b/tests/src/com/android/services/telephony/rcs/validator/OutgoingTransportStateValidatorTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2021 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.validator;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.SipMessage;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class OutgoingTransportStateValidatorTest {
+
+    private static final SipMessage TEST_MESSAGE = new SipMessage(
+            "INVITE sip:bob@biloxi.com SIP/2.0",
+            "Via: SIP/2.0/UDP pc33.atlanta.com;branch=z9hG4bK776asdhds\n"
+                    + "Max-Forwards: 70\n"
+                    + "To: Bob <sip:bob@biloxi.com>\n"
+                    + "From: Alice <sip:alice@atlanta.com>;tag=1928301774\n"
+                    + "Call-ID: a84b4c76e66710@pc33.atlanta.com\n"
+                    + "CSeq: 314159 INVITE\n"
+                    + "Contact: <sip:alice@pc33.atlanta.com>\n"
+                    + "Content-Type: application/sdp\n"
+                    + "Content-Length: 142",
+            new byte[0]);
+
+    @Test
+    public void testVerifyMessageAndUpdateState() {
+        OutgoingTransportStateValidator validator = new OutgoingTransportStateValidator();
+        ValidationResult result = validator.validate(TEST_MESSAGE);
+        assertFalse(result.isValidated);
+        assertEquals(SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED,
+                result.restrictedReason);
+
+        validator.open();
+        result = validator.validate(TEST_MESSAGE);
+        assertTrue(result.isValidated);
+
+        validator.restrict(
+                SipDelegateManager.MESSAGE_FAILURE_REASON_INTERNAL_DELEGATE_STATE_TRANSITION);
+        result = validator.validate(TEST_MESSAGE);
+        assertFalse(result.isValidated);
+        assertEquals(SipDelegateManager.MESSAGE_FAILURE_REASON_INTERNAL_DELEGATE_STATE_TRANSITION,
+                result.restrictedReason);
+
+        validator.close(SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED);
+        result = validator.validate(TEST_MESSAGE);
+        assertFalse(result.isValidated);
+        assertEquals(SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED,
+                result.restrictedReason);
+    }
+}