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);
+ }
+}