blob: 4cc474c3c394a3941fc03cf3d5b0682fcd966511 [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.libraries.rcs.simpleclient.service.chat;
import static com.android.libraries.rcs.simpleclient.protocol.cpim.CpimUtils.CPIM_CONTENT_TYPE;
import static com.android.libraries.rcs.simpleclient.service.chat.ChatServiceException.CODE_ERROR_UNSPECIFIED;
import static java.nio.charset.StandardCharsets.UTF_8;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.Nullable;
import com.android.libraries.rcs.simpleclient.SimpleRcsClientContext;
import com.android.libraries.rcs.simpleclient.protocol.cpim.CpimUtils;
import com.android.libraries.rcs.simpleclient.protocol.cpim.SimpleCpimMessage;
import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpChunk;
import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpChunk.Continuation;
import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpChunkHeader;
import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpConstants;
import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpManager;
import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpSession;
import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpUtils;
import com.android.libraries.rcs.simpleclient.protocol.sdp.SdpUtils;
import com.android.libraries.rcs.simpleclient.protocol.sdp.SimpleSdpMessage;
import com.android.libraries.rcs.simpleclient.protocol.sip.SipSessionConfiguration;
import com.android.libraries.rcs.simpleclient.protocol.sip.SipUtils;
import com.android.libraries.rcs.simpleclient.service.chat.ChatServiceException.ErrorCode;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import gov.nist.javax.sip.header.To;
import gov.nist.javax.sip.header.ims.PAssertedIdentityHeader;
import gov.nist.javax.sip.message.SIPRequest;
import gov.nist.javax.sip.message.SIPResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.text.ParseException;
import java.util.UUID;
import javax.sip.address.URI;
import javax.sip.message.Message;
import javax.sip.message.Request;
import javax.sip.message.Response;
/**
* Simple chat session implementation in order to send/receive a text message via SIP/MSRP
* connection. Currently, this supports only a outgoing CPM session.
*/
public class SimpleChatSession {
private static final String TAG = SimpleChatSession.class.getSimpleName();
private final SimpleRcsClientContext mContext;
private final MinimalCpmChatService mService;
private final MsrpManager mMsrpManager;
private final String mConversationId = UUID.randomUUID().toString();
private SettableFuture<Void> mStartFuture;
@Nullable
private SIPRequest mInviteRequest;
@Nullable
private URI mRemoteUri;
@Nullable
private SimpleSdpMessage mRemoteSdp;
@Nullable
private SimpleSdpMessage mLocalSdp;
@Nullable
private MsrpSession mMsrpSession;
@Nullable
private ChatSessionListener mListener;
public SimpleChatSession(
SimpleRcsClientContext context, MinimalCpmChatService service,
MsrpManager msrpManager) {
mService = service;
mContext = context;
mMsrpManager = msrpManager;
}
public URI getRemoteUri() {
return mRemoteUri;
}
/** Send a text message via MSRP session associated with this session. */
public void sendMessage(String msg) {
MsrpSession session = mMsrpSession;
if (session == null || mRemoteSdp == null || mLocalSdp == null) {
Log.e(TAG, "Session is not established");
return;
}
// Build a new CPIM message and send it out through the MSRP session.
SimpleCpimMessage cpim = CpimUtils.createForText(msg);
Log.i(TAG, "Encoded CPIM:" + cpim.encode());
byte[] content = cpim.encode().getBytes(UTF_8);
MsrpChunk msrpChunk =
MsrpChunk.newBuilder()
.method(MsrpChunk.Method.SEND)
.transactionId(MsrpUtils.generateRandomId())
.content(content)
.continuation(Continuation.COMPLETE)
.addHeader(MsrpConstants.HEADER_TO_PATH, mRemoteSdp.getPath().get())
.addHeader(MsrpConstants.HEADER_FROM_PATH, mLocalSdp.getPath().get())
.addHeader(MsrpConstants.HEADER_FAILURE_REPORT,
MsrpConstants.REPORT_VALUE_YES)
.addHeader(MsrpConstants.HEADER_SUCCESS_REPORT,
MsrpConstants.REPORT_VALUE_NO)
.addHeader(
MsrpConstants.HEADER_BYTE_RANGE,
String.format("1-%d/%d", content.length, content.length))
.addHeader(MsrpConstants.HEADER_MESSAGE_ID, MsrpUtils.generateRandomId())
.addHeader(MsrpConstants.HEADER_CONTENT_TYPE, CPIM_CONTENT_TYPE)
.build();
Log.i(TAG, "Send a MSRP chunk: " + msrpChunk);
Futures.addCallback(
session.send(msrpChunk),
new FutureCallback<MsrpChunk>() {
@Override
public void onSuccess(MsrpChunk result) {
if (result.responseCode() != 200) {
Log.d(
TAG,
"Received error response id="
+ result.transactionId()
+ " code="
+ result.responseCode());
}
}
@Override
public void onFailure(Throwable t) {
Log.d(TAG, "Failed to send msrp chunk", t);
}
},
MoreExecutors.directExecutor());
}
/** Start outgoing chat session. */
ListenableFuture<Void> start(String telUriContact) {
if (mStartFuture != null) {
return Futures.immediateFailedFuture(
new ChatServiceException("Session already started"));
}
SettableFuture<Void> future = SettableFuture.create();
mStartFuture = future;
mRemoteUri = SipUtils.createUri(telUriContact);
try {
SipSessionConfiguration configuration =
mContext.getSipSession().getSessionConfiguration();
SimpleSdpMessage sdp = SdpUtils.createSdpForMsrp(configuration.getLocalIpAddress(),
false);
SIPRequest invite =
SipUtils.buildInvite(
mContext.getSipSession().getSessionConfiguration(),
telUriContact,
mConversationId,
sdp.encode().getBytes(UTF_8));
mInviteRequest = invite;
mLocalSdp = sdp;
Futures.addCallback(
mService.sendSipRequest(invite, this),
new FutureCallback<Boolean>() {
@Override
public void onSuccess(Boolean result) {
Log.i(TAG, "onSuccess:" + result);
if (!result) {
notifyFailure("Failed to send INVITE", CODE_ERROR_UNSPECIFIED);
}
}
@Override
public void onFailure(Throwable t) {
Log.i(TAG, "onFailure:" + t.getMessage());
notifyFailure("Failed to send INVITE", CODE_ERROR_UNSPECIFIED);
}
},
MoreExecutors.directExecutor());
} catch (ParseException e) {
Log.e(TAG, e.getMessage());
e.printStackTrace();
return Futures.immediateFailedFuture(
new ChatServiceException("Failed to build INVITE"));
}
return future;
}
/** Start incoming chat session. */
ListenableFuture<Void> start(SIPRequest invite) {
mInviteRequest = invite;
int statusCode = Response.OK;
if (!SipUtils.hasSdpContent(invite)) {
statusCode = Response.NOT_ACCEPTABLE_HERE;
} else {
try {
mRemoteSdp = SimpleSdpMessage.parse(
new ByteArrayInputStream(invite.getRawContent()));
} catch (ParseException | IOException e) {
statusCode = Response.BAD_REQUEST;
}
}
updateRemoteUri(mInviteRequest);
SipSessionConfiguration configuration = mContext.getSipSession().getSessionConfiguration();
SimpleSdpMessage sdp = SdpUtils.createSdpForMsrp(configuration.getLocalIpAddress(), false);
// Automatically reply back to the invite by building a pre-canned response.
try {
SIPResponse response = SipUtils.buildInviteResponse(configuration, invite, statusCode,
sdp);
mLocalSdp = sdp;
return Futures.transform(
mService.sendSipResponse(response, this), result -> null,
MoreExecutors.directExecutor());
} catch (ParseException e) {
Log.e(TAG, "Exception while building response", e);
return Futures.immediateFailedFuture(e);
}
}
/** Terminate the current SIP session. */
public ListenableFuture<Void> terminate() {
if (mInviteRequest == null) {
return Futures.immediateFuture(null);
}
try {
if (mMsrpSession != null) {
mMsrpSession.terminate();
}
} catch (IOException e) {
return Futures.immediateFailedFuture(
new ChatServiceException(
"Exception while terminating MSRP session", CODE_ERROR_UNSPECIFIED));
}
try {
SettableFuture<Void> future = SettableFuture.create();
Futures.addCallback(
mService.sendSipRequest(SipUtils.buildBye(mInviteRequest), this),
new FutureCallback<Boolean>() {
@Override
public void onSuccess(Boolean result) {
future.set(null);
}
@Override
public void onFailure(Throwable t) {
future.setException(
new ChatServiceException("Failed to send BYE",
CODE_ERROR_UNSPECIFIED, t));
}
},
MoreExecutors.directExecutor());
return future;
} catch (ParseException e) {
return Futures.immediateFailedFuture(
new ChatServiceException("Failed to build BYE", CODE_ERROR_UNSPECIFIED));
}
}
void receiveMessage(Message msg) {
if (msg instanceof SIPRequest) {
handleSipRequest((SIPRequest) msg);
} else {
handleSipResponse((SIPResponse) msg);
}
}
private void handleSipRequest(SIPRequest request) {
SIPResponse response;
if (TextUtils.equals(request.getMethod(), Request.ACK)) {
// Terminating session established, start a msrp session.
if (mRemoteSdp != null) {
startMsrpSession(mRemoteSdp);
}
return;
}
if (TextUtils.equals(request.getMethod(), Request.BYE)) {
response = request.createResponse(Response.OK);
} else {
// Currently we support only INVITE and BYE.
response = request.createResponse(Response.METHOD_NOT_ALLOWED);
}
Futures.addCallback(
mService.sendSipResponse(response, this),
new FutureCallback<Boolean>() {
@Override
public void onSuccess(Boolean result) {
if (result) {
Log.d(
TAG,
"Response to Call-Id: "
+ response.getCallId().getCallId()
+ " sent successfully");
} else {
Log.d(TAG, "Failed to send response");
}
}
@Override
public void onFailure(Throwable t) {
Log.d(TAG, "Exception while sending response: ", t);
}
},
MoreExecutors.directExecutor());
}
private void handleSipResponse(SIPResponse response) {
int code = response.getStatusCode();
// Nothing to do for a provisional response.
if (response.isFinalResponse()) {
if (code == Response.OK) {
handle200OK(response);
} else {
handleNon200(response);
}
}
}
private void handleNon200(SIPResponse response) {
Log.d(TAG, "Received error response code=" + response.getStatusCode());
notifyFailure("Received non-200 INVITE response", CODE_ERROR_UNSPECIFIED);
}
private void handle200OK(SIPResponse response) {
if (!SipUtils.hasSdpContent(response)) {
notifyFailure("Content is not a SDP", CODE_ERROR_UNSPECIFIED);
return;
}
SimpleSdpMessage sdp;
try {
sdp = SimpleSdpMessage.parse(new ByteArrayInputStream(response.getRawContent()));
} catch (ParseException | IOException e) {
notifyFailure("Invalid SDP in INVITE", CODE_ERROR_UNSPECIFIED);
return;
}
if (mInviteRequest == null) {
notifyFailure("No INVITE request sent out", CODE_ERROR_UNSPECIFIED);
return;
}
SIPRequest ack = mInviteRequest.createAckRequest((To) response.getToHeader());
Futures.addCallback(
mService.sendSipRequest(ack, this),
new FutureCallback<Boolean>() {
@Override
public void onSuccess(Boolean result) {
if (result) {
startMsrpSession(sdp);
mRemoteSdp = sdp;
} else {
notifyFailure("Failed to send ACK", CODE_ERROR_UNSPECIFIED);
}
}
@Override
public void onFailure(Throwable t) {
notifyFailure("Failed to send ACK", CODE_ERROR_UNSPECIFIED);
}
},
MoreExecutors.directExecutor());
}
private void notifyFailure(String message, @ErrorCode int code) {
if (mStartFuture != null) {
mStartFuture.setException(new ChatServiceException(message, code));
mStartFuture = null;
}
}
private void notifySuccess() {
if (mStartFuture != null) {
mStartFuture.set(null);
mStartFuture = null;
}
}
private void startMsrpSession(SimpleSdpMessage remoteSdp) {
Log.d(TAG, "Start MSRP session: " + remoteSdp);
if (remoteSdp.getAddress().isPresent() && remoteSdp.getPort().isPresent()) {
String localIp = getLocalIp();
Futures.addCallback(
mMsrpManager.createMsrpSession(
remoteSdp.getAddress().get(), remoteSdp.getPort().getAsInt(), localIp,
0 /* localPort */, this::receiveMsrpChunk),
new FutureCallback<MsrpSession>() {
@Override
public void onSuccess(MsrpSession result) {
mMsrpSession = result;
sendEmptyPacket();
notifySuccess();
}
@Override
public void onFailure(Throwable t) {
Log.e(TAG, "Failed to create msrp session", t);
notifyFailure("Failed to establish msrp session",
CODE_ERROR_UNSPECIFIED);
terminate()
.addListener(
() -> Log.d(TAG, "Session terminated"),
MoreExecutors.directExecutor());
}
},
MoreExecutors.directExecutor());
} else {
Log.e(TAG, "Address or port is not present");
}
}
private void sendEmptyPacket() {
MsrpChunk msrpChunk =
MsrpChunk.newBuilder()
.method(MsrpChunk.Method.SEND)
.transactionId(MsrpUtils.generateRandomId())
.continuation(Continuation.COMPLETE)
.addHeader(MsrpConstants.HEADER_TO_PATH, mRemoteSdp.getPath().get())
.addHeader(MsrpConstants.HEADER_FROM_PATH, mLocalSdp.getPath().get())
.addHeader(MsrpConstants.HEADER_FAILURE_REPORT,
MsrpConstants.REPORT_VALUE_NO)
.addHeader(MsrpConstants.HEADER_SUCCESS_REPORT,
MsrpConstants.REPORT_VALUE_NO)
.addHeader(MsrpConstants.HEADER_BYTE_RANGE, "1/0-0")
.addHeader(MsrpConstants.HEADER_MESSAGE_ID, MsrpUtils.generateRandomId())
.build();
mMsrpSession.send(msrpChunk);
}
private String getLocalIp() {
SipSessionConfiguration configuration = mContext.getSipSession().getSessionConfiguration();
return configuration.getLocalIpAddress();
}
private void receiveMsrpChunk(MsrpChunk chunk) {
Log.d(TAG, "Received msrp= " + chunk + " conversation=" + mConversationId);
MsrpChunkHeader contentTypeHeader = chunk.header("Content-Type");
if (chunk.content().length == 0 || contentTypeHeader == null) {
Log.i(TAG, "No content or Content-Type header, drop it");
return;
}
String contentType = contentTypeHeader.value();
if ("message/cpim".equals(contentType)) {
Log.d(TAG, "Received CPIM: " + new String(chunk.content(), UTF_8));
try {
SimpleCpimMessage cpim = SimpleCpimMessage.parse(chunk.content());
if (mListener != null) {
mListener.onMessageReceived(cpim);
}
} catch (Exception e) {
Log.e(TAG, "Error while parsing cpim message.", e);
}
} else {
Log.w(TAG, contentType + " is not supported.");
}
}
/** Set new listener for this session. */
public void setListener(@Nullable ChatSessionListener listener) {
mListener = listener;
}
private void updateRemoteUri(SIPRequest request) {
PAssertedIdentityHeader pAssertedIdentityHeader =
(PAssertedIdentityHeader) request.getHeader("P-Asserted-Identity");
if (pAssertedIdentityHeader == null) {
mRemoteUri = request.getFrom().getAddress().getURI();
} else {
mRemoteUri = pAssertedIdentityHeader.getAddress().getURI();
}
}
}