blob: c42472de822ec874515afa7352ab8a2ec8f39b8c [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.SipDelegateImsConfiguration;
import android.telephony.ims.SipDelegateManager;
import android.telephony.ims.SipMessage;
import android.telephony.ims.aidl.ISipDelegate;
import android.telephony.ims.aidl.ISipDelegateMessageCallback;
import android.telephony.ims.stub.SipDelegate;
import android.util.LocalLog;
import android.util.Log;
import java.io.PrintWriter;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
/**
* Tracks the SIP message path both from the IMS application to the SipDelegate and from the
* SipDelegate back to the IMS Application.
* <p>
* Responsibilities include:
* 1) Queue incoming and outgoing SIP messages and deliver to IMS application and SipDelegate in
* order. If there is an error delivering the message, notify the caller.
* 2) TODO Perform basic validation of outgoing messages.
* 3) TODO Record the status of ongoing SIP Dialogs and trigger the completion of pending
* consumers when they are finished or call closeDialog to clean up the SIP
* dialogs that did not complete within the allotted timeout time.
* <p>
* Note: This handles incoming binder calls, so all calls from other processes should be handled on
* the provided Executor.
*/
public class MessageTransportStateTracker implements DelegateBinderStateManager.StateCallback {
private static final String TAG = "MessageST";
/**
* Communicates the result of verifying whether a SIP message should be sent based on the
* contents of the SIP message as well as if the transport is in an available state for the
* intended recipient of the message.
*/
private static class VerificationResult {
public static final VerificationResult SUCCESS = new VerificationResult();
/**
* If {@code true}, the requested SIP message has been verified to be sent to the remote. If
* {@code false}, the SIP message has failed verification and should not be sent to the
* result. The {@link #restrictedReason} field will contain the reason for the verification
* failure.
*/
public final boolean isVerified;
/**
* The reason associated with why the SIP message was not verified and generated a
* {@code false} result for {@link #isVerified}.
*/
public final int restrictedReason;
/**
* Communicates a verified result of success. Use {@link #SUCCESS} instead.
*/
private VerificationResult() {
isVerified = true;
restrictedReason = SipDelegateManager.MESSAGE_FAILURE_REASON_UNKNOWN;
}
/**
* The result of verifying that the SIP Message should be sent.
* @param reason The reason associated with why the SIP message was not verified and
* generated a {@code false} result for {@link #isVerified}.
*/
VerificationResult(@SipDelegateManager.MessageFailureReason int reason) {
isVerified = false;
restrictedReason = reason;
}
}
// SipDelegateConnection(IMS Application) -> SipDelegate(ImsService)
private final ISipDelegate.Stub mSipDelegateConnection = new ISipDelegate.Stub() {
/**
* The IMS application is acknowledging that it has successfully received and processed an
* incoming SIP message sent by the SipDelegate in
* {@link ISipDelegateMessageCallback#onMessageReceived(SipMessage)}.
*/
@Override
public void notifyMessageReceived(String viaTransactionId) {
long token = Binder.clearCallingIdentity();
try {
mExecutor.execute(() -> {
if (mSipDelegate == null) {
logw("notifyMessageReceived called when SipDelegate is not associated for "
+ "transaction id: " + viaTransactionId);
return;
}
try {
// TODO track the SIP Dialogs created/destroyed on the associated
// SipDelegate.
mSipDelegate.notifyMessageReceived(viaTransactionId);
} catch (RemoteException e) {
logw("SipDelegate not available when notifyMessageReceived was called "
+ "for transaction id: " + viaTransactionId);
}
});
} finally {
Binder.restoreCallingIdentity(token);
}
}
/**
* The IMS application is acknowledging that it received an incoming SIP message sent by the
* SipDelegate in {@link ISipDelegateMessageCallback#onMessageReceived(SipMessage)} but it
* was unable to process it.
*/
@Override
public void notifyMessageReceiveError(String viaTransactionId, int reason) {
long token = Binder.clearCallingIdentity();
try {
mExecutor.execute(() -> {
if (mSipDelegate == null) {
logw("notifyMessageReceiveError called when SipDelegate is not associated "
+ "for transaction id: " + viaTransactionId);
return;
}
try {
// TODO track the SIP Dialogs created/destroyed on the associated
// SipDelegate.
mSipDelegate.notifyMessageReceiveError(viaTransactionId, reason);
} catch (RemoteException e) {
logw("SipDelegate not available when notifyMessageReceiveError was called "
+ "for transaction id: " + viaTransactionId);
}
});
} finally {
Binder.restoreCallingIdentity(token);
}
}
/**
* The IMS application is sending an outgoing SIP message to the SipDelegate to be processed
* and sent over the network.
*/
@Override
public void sendMessage(SipMessage sipMessage, long configVersion) {
long token = Binder.clearCallingIdentity();
try {
mExecutor.execute(() -> {
VerificationResult result = verifyOutgoingMessage(sipMessage);
if (!result.isVerified) {
notifyDelegateSendError("Outgoing messages restricted", sipMessage,
result.restrictedReason);
return;
}
try {
// TODO track the SIP Dialogs created/destroyed on the associated
// SipDelegate.
mSipDelegate.sendMessage(sipMessage, configVersion);
logi("sendMessage: message sent - " + sipMessage + ", configVersion: "
+ configVersion);
} catch (RemoteException e) {
notifyDelegateSendError("RemoteException: " + e, sipMessage,
SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_DEAD);
}
});
} finally {
Binder.restoreCallingIdentity(token);
}
}
/**
* The SipDelegateConnection is requesting that the resources associated with an ongoing SIP
* dialog be released as the SIP dialog is now closed.
*/
@Override
public void closeDialog(String callId) {
long token = Binder.clearCallingIdentity();
try {
mExecutor.execute(() -> {
if (mSipDelegate == null) {
logw("closeDialog called when SipDelegate is not associated, callId: "
+ callId);
return;
}
try {
// TODO track the SIP Dialogs created/destroyed on the associated
// SipDelegate.
mSipDelegate.closeDialog(callId);
} catch (RemoteException e) {
logw("SipDelegate not available when closeDialog was called "
+ "for call id: " + callId);
}
});
} finally {
Binder.restoreCallingIdentity(token);
}
}
};
// SipDelegate(ImsService) -> SipDelegateConnection(IMS Application)
private final ISipDelegateMessageCallback.Stub mDelegateConnectionMessageCallback =
new ISipDelegateMessageCallback.Stub() {
/**
* An Incoming SIP Message has been received by the SipDelegate and is being routed
* to the IMS application for processing.
* <p>
* IMS application will call {@link ISipDelegate#notifyMessageReceived(String)} to
* acknowledge receipt of this incoming message.
*/
@Override
public void onMessageReceived(SipMessage message) {
long token = Binder.clearCallingIdentity();
try {
mExecutor.execute(() -> {
VerificationResult result = verifyIncomingMessage(message);
if (!result.isVerified) {
notifyAppReceiveError("Incoming messages restricted", message,
result.restrictedReason);
return;
}
try {
// TODO track the SIP Dialogs created/destroyed on the associated
// SipDelegate.
mAppCallback.onMessageReceived(message);
logi("onMessageReceived: received " + message);
} catch (RemoteException e) {
notifyAppReceiveError("RemoteException: " + e, message,
SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_DEAD);
}
});
} finally {
Binder.restoreCallingIdentity(token);
}
}
/**
* An outgoing SIP message sent previously by the SipDelegateConnection to the SipDelegate
* using {@link ISipDelegate#sendMessage(SipMessage, int)} as been successfully sent.
*/
@Override
public void onMessageSent(String viaTransactionId) {
long token = Binder.clearCallingIdentity();
try {
mExecutor.execute(() -> {
if (mSipDelegate == null) {
logw("Unexpected state, onMessageSent called when SipDelegate is not "
+ "associated");
}
try {
mAppCallback.onMessageSent(viaTransactionId);
} catch (RemoteException e) {
logw("Error sending onMessageSent to SipDelegateConnection, remote not"
+ "available for transaction ID: " + viaTransactionId);
}
});
} finally {
Binder.restoreCallingIdentity(token);
}
}
/**
* An outgoing SIP message sent previously by the SipDelegateConnection to the SipDelegate
* using {@link ISipDelegate#sendMessage(SipMessage, int)} failed to be sent.
*/
@Override
public void onMessageSendFailure(String viaTransactionId, int reason) {
long token = Binder.clearCallingIdentity();
try {
mExecutor.execute(() -> {
if (mSipDelegate == null) {
logw("Unexpected state, onMessageSendFailure called when SipDelegate is not"
+ "associated");
}
try {
mAppCallback.onMessageSendFailure(viaTransactionId, reason);
} catch (RemoteException e) {
logw("Error sending onMessageSendFailure to SipDelegateConnection, remote"
+ " not available for transaction ID: " + viaTransactionId);
}
});
} finally {
Binder.restoreCallingIdentity(token);
}
}
};
private final ISipDelegateMessageCallback mAppCallback;
private final Executor mExecutor;
private final int mSubId;
private final LocalLog mLocalLog = new LocalLog(SipTransportController.LOG_SIZE);
private ISipDelegate mSipDelegate;
private Consumer<Boolean> mPendingClosedConsumer;
private int mDelegateClosingReason = -1;
private int mDelegateClosedReason = -1;
public MessageTransportStateTracker(int subId, Executor executor,
ISipDelegateMessageCallback appMessageCallback) {
mSubId = subId;
mAppCallback = appMessageCallback;
mExecutor = executor;
}
@Override
public void onRegistrationStateChanged(DelegateRegistrationState registrationState) {
// TODO: integrate registration changes to SipMessage verification checks.
}
@Override
public void onImsConfigurationChanged(SipDelegateImsConfiguration config) {
// Not needed for this Tracker
}
/**
* Open the transport and allow SIP messages to be sent/received on the delegate specified.
* @param delegate The delegate connection to send SIP messages to on the ImsService.
* @param deniedFeatureTags Feature tags that have been denied. Outgoing SIP messages relating
* to these tags will be denied.
*/
public void openTransport(ISipDelegate delegate, Set<FeatureTagState> deniedFeatureTags) {
mSipDelegate = delegate;
mDelegateClosingReason = -1;
mDelegateClosedReason = -1;
// TODO: integrate denied tags to SipMessage verification checks.
}
/** Dump state about this tracker that should be included in the dumpsys */
public void dump(PrintWriter printWriter) {
printWriter.println("Most recent logs:");
mLocalLog.dump(printWriter);
}
/**
* @return SipDelegate implementation to be sent to IMS application.
*/
public ISipDelegate getDelegateConnection() {
return mSipDelegateConnection;
}
/**
* @return MessageCallback implementation to be sent to the ImsService.
*/
public ISipDelegateMessageCallback getMessageCallback() {
return mDelegateConnectionMessageCallback;
}
/**
* Gradually close all SIP Dialogs by:
* 1) denying all new outgoing SIP Dialog requests with the reason specified and
* 2) only allowing existing SIP Dialogs to continue.
* <p>
* This will allow traffic to continue on existing SIP Dialogs until a BYE is sent and the
* SIP Dialogs are closed or a timeout is hit and {@link SipDelegate#closeDialog(String)} is
* forcefully called on all open SIP Dialogs.
* <p>
* Any outgoing out-of-dialog traffic on this transport will be denied with the provided reason.
* <p>
* Incoming out-of-dialog traffic will continue to be set up until the SipDelegate is fully
* closed.
* @param delegateClosingReason The reason code to return to
* {@link DelegateMessageCallback#onMessageSendFailure(String, int)} if a new out-of-dialog SIP
* message is received while waiting for existing Dialogs.
* @param closedReason reason to return to new outgoing SIP messages via
* {@link SipDelegate#notifyMessageReceiveError(String, int)} once the transport
* transitions to the fully closed state.
* @param resultConsumer The consumer called when the message transport has been closed. It will
* return {@code true} if the procedure completed successfully or {@link false} if the
* transport needed to be closed forcefully due to the application not responding before
* a timeout occurred.
*/
public void closeGracefully(int delegateClosingReason, int closedReason,
Consumer<Boolean> resultConsumer) {
mDelegateClosingReason = delegateClosingReason;
mPendingClosedConsumer = resultConsumer;
mExecutor.execute(() -> {
// TODO: Track SIP Dialogs and complete when there are no SIP dialogs open anymore or
// the timeout occurs.
mPendingClosedConsumer.accept(true);
mPendingClosedConsumer = null;
closeTransport(closedReason);
});
}
/**
* Close all ongoing SIP Dialogs immediately and respond to any incoming/outgoing messages with
* the provided reason.
* @param closedReason The failure reason to provide to incoming/outgoing SIP messages
* if an attempt is made to send/receive a message after this method is called.
*/
public void close(int closedReason) {
closeTransport(closedReason);
}
// Clean up all state related to the existing SipDelegate immediately.
private void closeTransport(int closedReason) {
// TODO: add logic to forcefully close open SIP dialogs once they are being tracked.
mSipDelegate = null;
if (mPendingClosedConsumer != null) {
mExecutor.execute(() -> {
logw("closeTransport: transport close forced with pending consumer.");
mPendingClosedConsumer.accept(false /*closedGracefully*/);
mPendingClosedConsumer = null;
});
}
mDelegateClosingReason = -1;
mDelegateClosedReason = closedReason;
}
private VerificationResult verifyOutgoingMessage(SipMessage message) {
if (mDelegateClosingReason > -1) {
return new VerificationResult(mDelegateClosingReason);
}
if (mDelegateClosedReason > -1) {
return new VerificationResult(mDelegateClosedReason);
}
if (mSipDelegate == null) {
logw("sendMessage called when SipDelegate is not associated." + message);
return new VerificationResult(SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_DEAD);
}
return VerificationResult.SUCCESS;
}
private VerificationResult verifyIncomingMessage(SipMessage message) {
// Do not restrict incoming based on closing reason.
if (mDelegateClosedReason > -1) {
return new VerificationResult(mDelegateClosedReason);
}
return VerificationResult.SUCCESS;
}
private void notifyDelegateSendError(String logReason, SipMessage message, int reasonCode) {
// TODO parse SipMessage header for viaTransactionId.
logw("Error sending SipMessage[id: " + null + ", code: " + reasonCode + "] -> SipDelegate "
+ "for reason: " + logReason);
try {
mAppCallback.onMessageSendFailure(null, reasonCode);
} catch (RemoteException e) {
logw("notifyDelegateSendError, SipDelegate is not available: " + e);
}
}
private void notifyAppReceiveError(String logReason, SipMessage message, int reasonCode) {
// TODO parse SipMessage header for viaTransactionId.
logw("Error sending SipMessage[id: " + null + ", code: " + reasonCode + "] -> "
+ "SipDelegateConnection for reason: " + logReason);
try {
mSipDelegate.notifyMessageReceiveError(null, reasonCode);
} catch (RemoteException e) {
logw("notifyAppReceiveError, SipDelegate is not available: " + e);
}
}
private void logi(String log) {
Log.w(SipTransportController.LOG_TAG, TAG + "[" + mSubId + "] " + log);
mLocalLog.log("[I] " + log);
}
private void logw(String log) {
Log.w(SipTransportController.LOG_TAG, TAG + "[" + mSubId + "] " + log);
mLocalLog.log("[W] " + log);
}
}