blob: 0d4265ab618e4e7d14d58aec859443092881d416 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.services.telephony.rcs;
import android.os.Binder;
import android.os.RemoteException;
import android.telephony.ims.DelegateMessageCallback;
import android.telephony.ims.DelegateRegistrationState;
import android.telephony.ims.FeatureTagState;
import android.telephony.ims.SipDelegateConfiguration;
import android.telephony.ims.SipDelegateImsConfiguration;
import android.telephony.ims.SipDelegateManager;
import android.telephony.ims.SipMessage;
import android.telephony.ims.aidl.ISipDelegate;
import android.telephony.ims.aidl.ISipDelegateMessageCallback;
import android.telephony.ims.stub.SipDelegate;
import android.util.LocalLog;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.SipMessageParsingUtils;
import com.android.internal.util.IndentingPrintWriter;
import com.android.phone.RcsProvisioningMonitor;
import com.android.services.telephony.rcs.validator.ValidationResult;
import java.io.PrintWriter;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Consumer;
/**
* Wraps the SIP message path both from the IMS application to the SipDelegate and from the
* SipDelegate back to the IMS Application.
* <p>
* 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 TransportSipMessageValidator} 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 MessageTransportWrapper implements DelegateBinderStateManager.StateCallback {
private static final String TAG = "MessageTW";
// SipDelegateConnection(IMS Application) -> SipDelegate(ImsService)
private final ISipDelegate.Stub mSipDelegateConnection = new ISipDelegate.Stub() {
/**
* The IMS application is acknowledging that it has successfully received and processed an
* incoming SIP message sent by the SipDelegate in
* {@link ISipDelegateMessageCallback#onMessageReceived(SipMessage)}.
*/
@Override
public void notifyMessageReceived(String viaTransactionId) {
long token = Binder.clearCallingIdentity();
try {
mExecutor.execute(() -> {
if (mSipDelegate == null) {
logw("notifyMessageReceived called when SipDelegate is not associated for "
+ "transaction id: " + viaTransactionId);
return;
}
try {
mSipSessionTracker.acknowledgePendingMessage(viaTransactionId);
mSipDelegate.notifyMessageReceived(viaTransactionId);
} catch (RemoteException e) {
logw("SipDelegate not available when notifyMessageReceived was called "
+ "for transaction id: " + viaTransactionId);
}
});
} finally {
Binder.restoreCallingIdentity(token);
}
}
/**
* The IMS application is acknowledging that it received an incoming SIP message sent by the
* SipDelegate in {@link ISipDelegateMessageCallback#onMessageReceived(SipMessage)} but it
* was unable to process it.
*/
@Override
public void notifyMessageReceiveError(String viaTransactionId, int reason) {
long token = Binder.clearCallingIdentity();
try {
mExecutor.execute(() -> {
if (mSipDelegate == null) {
logw("notifyMessageReceiveError called when SipDelegate is not associated "
+ "for transaction id: " + viaTransactionId);
return;
}
try {
mSipSessionTracker.notifyPendingMessageFailed(viaTransactionId);
mSipDelegate.notifyMessageReceiveError(viaTransactionId, reason);
} catch (RemoteException e) {
logw("SipDelegate not available when notifyMessageReceiveError was called "
+ "for transaction id: " + viaTransactionId);
}
});
} finally {
Binder.restoreCallingIdentity(token);
}
}
/**
* The IMS application is sending an outgoing SIP message to the SipDelegate to be processed
* and sent over the network.
*/
@Override
public void sendMessage(SipMessage sipMessage, long configVersion) {
long token = Binder.clearCallingIdentity();
try {
mExecutor.execute(() -> {
ValidationResult result =
mSipSessionTracker.verifyOutgoingMessage(sipMessage, configVersion);
result = maybeOverrideValidationForTesting(result);
if (!result.isValidated) {
notifyDelegateSendError("Outgoing - " + result.logReason,
sipMessage, result.restrictedReason);
return;
}
try {
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);
} catch (RemoteException e) {
notifyDelegateSendError("RemoteException: " + e, sipMessage,
SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_DEAD);
}
});
} finally {
Binder.restoreCallingIdentity(token);
}
}
/**
* The SipDelegateConnection is requesting that the resources associated with an ongoing SIP
* dialog be released as the SIP dialog is now closed.
*/
@Override
public void cleanupSession(String callId) {
long token = Binder.clearCallingIdentity();
try {
mExecutor.execute(() -> cleanupSessionInternal(callId));
} finally {
Binder.restoreCallingIdentity(token);
}
}
};
// SipDelegate(ImsService) -> SipDelegateConnection(IMS Application)
private final ISipDelegateMessageCallback.Stub mDelegateConnectionMessageCallback =
new ISipDelegateMessageCallback.Stub() {
/**
* An Incoming SIP Message has been received by the SipDelegate and is being routed
* to the IMS application for processing.
* <p>
* IMS application will call {@link ISipDelegate#notifyMessageReceived(String)} to
* acknowledge receipt of this incoming message.
*/
@Override
public void onMessageReceived(SipMessage message) {
long token = Binder.clearCallingIdentity();
try {
mExecutor.execute(() -> {
ValidationResult result = mSipSessionTracker.verifyIncomingMessage(message);
if (!result.isValidated) {
notifyAppReceiveError("Incoming - " + result.logReason, message,
result.restrictedReason);
return;
}
try {
mAppCallback.onMessageReceived(message);
} catch (RemoteException e) {
notifyAppReceiveError("RemoteException: " + e, message,
SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_DEAD);
}
});
} finally {
Binder.restoreCallingIdentity(token);
}
}
/**
* An outgoing SIP message sent previously by the SipDelegateConnection to the SipDelegate
* using {@link ISipDelegate#sendMessage(SipMessage, long)} as been successfully sent.
*/
@Override
public void onMessageSent(String viaTransactionId) {
long token = Binder.clearCallingIdentity();
try {
mExecutor.execute(() -> {
if (mSipDelegate == null) {
logw("Unexpected state, onMessageSent called when SipDelegate is not "
+ "associated");
}
try {
mSipSessionTracker.acknowledgePendingMessage(viaTransactionId);
mAppCallback.onMessageSent(viaTransactionId);
} catch (RemoteException e) {
logw("Error sending onMessageSent to SipDelegateConnection, remote not"
+ "available for transaction ID: " + viaTransactionId);
}
});
} finally {
Binder.restoreCallingIdentity(token);
}
}
/**
* An outgoing SIP message sent previously by the SipDelegateConnection to the SipDelegate
* using {@link ISipDelegate#sendMessage(SipMessage, long)} failed to be sent.
*/
@Override
public void onMessageSendFailure(String viaTransactionId, int reason) {
long token = Binder.clearCallingIdentity();
try {
mExecutor.execute(() -> {
if (mSipDelegate == null) {
logw("Unexpected state, onMessageSendFailure called when SipDelegate is not"
+ "associated");
}
try {
mSipSessionTracker.notifyPendingMessageFailed(viaTransactionId);
mAppCallback.onMessageSendFailure(viaTransactionId, reason);
} catch (RemoteException e) {
logw("Error sending onMessageSendFailure to SipDelegateConnection, remote"
+ " not available for transaction ID: " + viaTransactionId);
}
});
} finally {
Binder.restoreCallingIdentity(token);
}
}
};
/**
* Interface for injecting validator override dependencies for testing.
*/
@VisibleForTesting
public interface ValidatorOverride {
/**
* @return {@code null} if the validation result should not be overridden, {@code true} if
* the validation result should always pass, {@code false} if the validation result should
* always fail.
*/
Boolean getValidatorOverrideState();
}
private final ValidatorOverride mValidatorOverride;
private final ISipDelegateMessageCallback mAppCallback;
private final Executor mExecutor;
private final int mSubId;
private final TransportSipMessageValidator mSipSessionTracker;
private final LocalLog mLocalLog = new LocalLog(SipTransportController.LOG_SIZE);
private ISipDelegate mSipDelegate;
public MessageTransportWrapper(int subId, ScheduledExecutorService executor,
ISipDelegateMessageCallback appMessageCallback) {
mSubId = subId;
mAppCallback = appMessageCallback;
mExecutor = executor;
mSipSessionTracker = new TransportSipMessageValidator(subId, executor);
mValidatorOverride = () -> RcsProvisioningMonitor.getInstance()
.getImsFeatureValidationOverride(mSubId);
}
/**
* Mock out dependencies for unit testing.
*/
@VisibleForTesting
public MessageTransportWrapper(int subId, ScheduledExecutorService executor,
ISipDelegateMessageCallback appMessageCallback,
TransportSipMessageValidator sipSessionTracker) {
mSubId = subId;
mAppCallback = appMessageCallback;
mExecutor = executor;
mSipSessionTracker = sipSessionTracker;
// Remove links to static methods calls querying overrides for testing.
mValidatorOverride = () -> null;
}
@Override
public void onRegistrationStateChanged(DelegateRegistrationState registrationState) {
mSipSessionTracker.onRegistrationStateChanged((callIds) -> {
for (String id : callIds) {
cleanupSessionInternal(id);
}
}, registrationState);
}
@Override
public void onImsConfigurationChanged(SipDelegateImsConfiguration config) {
mSipSessionTracker.onImsConfigurationChanged(config);
}
@Override
public void onConfigurationChanged(SipDelegateConfiguration config) {
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<String> supportedFeatureTags,
Set<FeatureTagState> deniedFeatureTags) {
logi("openTransport: delegate=" + delegate + ", supportedTags=" + supportedFeatureTags
+ ", deniedTags=" + deniedFeatureTags);
mSipSessionTracker.onTransportOpened(supportedFeatureTags, deniedFeatureTags);
mSipDelegate = delegate;
}
/** Dump state about this tracker that should be included in the dumpsys */
public void dump(PrintWriter printWriter) {
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();
}
/**
* @return SipDelegate implementation to be sent to IMS application.
*/
public ISipDelegate getDelegateConnection() {
return mSipDelegateConnection;
}
/**
* @return The remote application's message callback.
*/
public ISipDelegateMessageCallback getAppMessageCallback() {
return mAppCallback;
}
/**
* @return MessageCallback implementation to be sent to the ImsService.
*/
public ISipDelegateMessageCallback getMessageCallback() {
return mDelegateConnectionMessageCallback;
}
/**
* Gradually close all SIP Sessions by:
* 1) denying all new outgoing SIP Dialog requests with the reason specified and
* 2) only allowing existing SIP Sessions to continue.
* <p>
* This will allow traffic to continue on existing SIP Sessions until a BYE is sent and the
* corresponding SIP Dialogs are closed or a timeout is hit and
* {@link SipDelegate#cleanupSession(String)} (String)} is forcefully called on all open SIP
* sessions.
* <p>
* Any outgoing out-of-dialog traffic on this transport will be denied with the provided reason.
* <p>
* Incoming out-of-dialog traffic will continue to be set up until the SipDelegate is fully
* closed.
* @param delegateClosingReason The reason code to return to
* {@link DelegateMessageCallback#onMessageSendFailure(String, int)} if a new out-of-dialog SIP
* message is received while waiting for existing Dialogs.
* @param closedReason reason to return to new outgoing SIP messages via
* {@link SipDelegate#notifyMessageReceiveError(String, int)} once the transport
* transitions to the fully closed state.
* @param resultConsumer The consumer called when the message transport has been closed. It will
* return {@code true} if the procedure completed successfully or {@link false} if the
* transport needed to be closed forcefully due to the application not responding before
* a timeout occurred.
*/
public void closeGracefully(int delegateClosingReason, int closedReason,
Consumer<Boolean> resultConsumer) {
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);
}
/**
* Close all ongoing SIP sessions immediately and respond to any incoming/outgoing messages with
* the provided reason.
* @param closedReason The failure reason to provide to incoming/outgoing SIP messages
* if an attempt is made to send/receive a message after this method is called.
*/
public void close(int closedReason) {
Set<String> openSessions = mSipSessionTracker.closeSessions(closedReason);
logi("close: closedReason=" + closedReason + "open call IDs:{" + openSessions + "}");
closeTransport(openSessions);
}
// Clean up all state related to the existing SipDelegate immediately.
private void closeTransport(Set<String> openCallIds) {
for (String id : openCallIds) {
cleanupSessionInternal(id);
}
mSipDelegate = null;
}
private void cleanupSessionInternal(String callId) {
logi("cleanupSessionInternal: clean up session with callId: " + callId);
try {
if (mSipDelegate == null) {
logw("cleanupSessionInternal: SipDelegate is not associated, callId: " + callId);
} else {
// This will close the transport, so call cleanup on ImsService first.
mSipDelegate.cleanupSession(callId);
}
} catch (RemoteException e) {
logw("cleanupSessionInternal: remote not available when cleanupSession was called "
+ "for call id: " + callId);
}
mSipSessionTracker.onSipSessionCleanup(callId);
}
private ValidationResult maybeOverrideValidationForTesting(ValidationResult result) {
Boolean isValidatedOverride = mValidatorOverride.getValidatorOverrideState();
if (isValidatedOverride == null) {
return result;
}
if (isValidatedOverride) {
return ValidationResult.SUCCESS;
} else if (result.isValidated) {
// if override is set to false and the original result was validated, return a new
// restricted result with UNKNOWN reason.
return new ValidationResult(SipDelegateManager.MESSAGE_FAILURE_REASON_UNKNOWN,
"validation failed due to a testing override being set");
}
return result;
}
private void notifyDelegateSendError(String logReason, SipMessage message, int reasonCode) {
String transactionId = SipMessageParsingUtils.getTransactionId(message.getHeaderSection());
logi("Error sending SipMessage[id: " + transactionId + ", code: " + reasonCode
+ "] -> SipDelegate for reason: " + logReason);
try {
mAppCallback.onMessageSendFailure(transactionId, reasonCode);
} catch (RemoteException e) {
logw("notifyDelegateSendError, SipDelegate is not available: " + e);
}
}
private void notifyAppReceiveError(String logReason, SipMessage message, int reasonCode) {
String transactionId = SipMessageParsingUtils.getTransactionId(message.getHeaderSection());
logi("Error sending SipMessage[id: " + transactionId + ", code: " + reasonCode + "] -> "
+ "SipDelegateConnection for reason: " + logReason);
try {
mSipDelegate.notifyMessageReceiveError(transactionId, reasonCode);
} catch (RemoteException e) {
logw("notifyAppReceiveError, SipDelegate is not available: " + e);
}
}
private void logi(String log) {
Log.w(SipTransportController.LOG_TAG, TAG + "[" + mSubId + "] " + log);
mLocalLog.log("[I] " + log);
}
private void logw(String log) {
Log.w(SipTransportController.LOG_TAG, TAG + "[" + mSubId + "] " + log);
mLocalLog.log("[W] " + log);
}
}