1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.libraries.rcs.simpleclient.service.chat; 18 19 import static com.android.libraries.rcs.simpleclient.protocol.cpim.CpimUtils.CPIM_CONTENT_TYPE; 20 import static com.android.libraries.rcs.simpleclient.service.chat.ChatServiceException.CODE_ERROR_SEND_MESSAGE_FAILED; 21 import static com.android.libraries.rcs.simpleclient.service.chat.ChatServiceException.CODE_ERROR_UNSPECIFIED; 22 23 import static java.nio.charset.StandardCharsets.UTF_8; 24 25 import android.text.TextUtils; 26 import android.util.Log; 27 28 import androidx.annotation.Nullable; 29 30 import com.android.libraries.rcs.simpleclient.SimpleRcsClientContext; 31 import com.android.libraries.rcs.simpleclient.protocol.cpim.CpimUtils; 32 import com.android.libraries.rcs.simpleclient.protocol.cpim.SimpleCpimMessage; 33 import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpChunk; 34 import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpChunk.Continuation; 35 import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpChunkHeader; 36 import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpConstants; 37 import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpManager; 38 import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpSession; 39 import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpUtils; 40 import com.android.libraries.rcs.simpleclient.protocol.sdp.SdpUtils; 41 import com.android.libraries.rcs.simpleclient.protocol.sdp.SimpleSdpMessage; 42 import com.android.libraries.rcs.simpleclient.protocol.sip.SipSessionConfiguration; 43 import com.android.libraries.rcs.simpleclient.protocol.sip.SipUtils; 44 import com.android.libraries.rcs.simpleclient.service.chat.ChatServiceException.ErrorCode; 45 46 import com.google.common.util.concurrent.FutureCallback; 47 import com.google.common.util.concurrent.Futures; 48 import com.google.common.util.concurrent.ListenableFuture; 49 import com.google.common.util.concurrent.MoreExecutors; 50 import com.google.common.util.concurrent.SettableFuture; 51 52 import gov.nist.javax.sip.header.To; 53 import gov.nist.javax.sip.header.ims.PAssertedIdentityHeader; 54 import gov.nist.javax.sip.message.SIPRequest; 55 import gov.nist.javax.sip.message.SIPResponse; 56 57 import java.io.ByteArrayInputStream; 58 import java.io.IOException; 59 import java.text.ParseException; 60 import java.util.UUID; 61 62 import javax.sip.address.URI; 63 import javax.sip.message.Message; 64 import javax.sip.message.Request; 65 import javax.sip.message.Response; 66 67 /** 68 * Simple chat session implementation in order to send/receive a text message via SIP/MSRP 69 * connection. Currently, this supports only a outgoing CPM session. 70 */ 71 public class SimpleChatSession { 72 private static final String TAG = SimpleChatSession.class.getSimpleName(); 73 private final SimpleRcsClientContext mContext; 74 private final MinimalCpmChatService mService; 75 private final MsrpManager mMsrpManager; 76 private final String mConversationId = UUID.randomUUID().toString(); 77 private SettableFuture<Void> mStartFuture; 78 @Nullable 79 private SIPRequest mInviteRequest; 80 @Nullable 81 private URI mRemoteUri; 82 @Nullable 83 private SimpleSdpMessage mRemoteSdp; 84 @Nullable 85 private SimpleSdpMessage mLocalSdp; 86 @Nullable 87 private MsrpSession mMsrpSession; 88 @Nullable 89 private ChatSessionListener mListener; 90 91 SimpleChatSession( SimpleRcsClientContext context, MinimalCpmChatService service, MsrpManager msrpManager)92 public SimpleChatSession( 93 SimpleRcsClientContext context, MinimalCpmChatService service, 94 MsrpManager msrpManager) { 95 mService = service; 96 mContext = context; 97 mMsrpManager = msrpManager; 98 } 99 getRemoteUri()100 public URI getRemoteUri() { 101 return mRemoteUri; 102 } 103 104 /** Send a text message via MSRP session associated with this session. */ sendMessage(String msg)105 public ListenableFuture<Void> sendMessage(String msg) { 106 MsrpSession session = mMsrpSession; 107 if (session == null || mRemoteSdp == null || mLocalSdp == null) { 108 Log.e(TAG, "Session is not established"); 109 return Futures.immediateFailedFuture( 110 new IllegalStateException("Session is not established")); 111 } 112 113 // Build a new CPIM message and send it out through the MSRP session. 114 SimpleCpimMessage cpim = CpimUtils.createForText(msg); 115 Log.i(TAG, "Encoded CPIM:" + cpim.encode()); 116 117 byte[] content = cpim.encode().getBytes(UTF_8); 118 MsrpChunk msrpChunk = 119 MsrpChunk.newBuilder() 120 .method(MsrpChunk.Method.SEND) 121 .transactionId(MsrpUtils.generateRandomId()) 122 .content(content) 123 .continuation(Continuation.COMPLETE) 124 .addHeader(MsrpConstants.HEADER_TO_PATH, mRemoteSdp.getPath().get()) 125 .addHeader(MsrpConstants.HEADER_FROM_PATH, mLocalSdp.getPath().get()) 126 .addHeader(MsrpConstants.HEADER_FAILURE_REPORT, 127 MsrpConstants.REPORT_VALUE_YES) 128 .addHeader(MsrpConstants.HEADER_SUCCESS_REPORT, 129 MsrpConstants.REPORT_VALUE_NO) 130 .addHeader( 131 MsrpConstants.HEADER_BYTE_RANGE, 132 String.format("1-%d/%d", content.length, content.length)) 133 .addHeader(MsrpConstants.HEADER_MESSAGE_ID, MsrpUtils.generateRandomId()) 134 .addHeader(MsrpConstants.HEADER_CONTENT_TYPE, CPIM_CONTENT_TYPE) 135 .build(); 136 137 Log.i(TAG, "Send a MSRP chunk: " + msrpChunk); 138 return Futures.transformAsync(session.send(msrpChunk), result -> { 139 if (result == null) { 140 return Futures.immediateFailedFuture( 141 new ChatServiceException("Failed to send a chunk", 142 CODE_ERROR_SEND_MESSAGE_FAILED)); 143 } 144 if (result.responseCode() != 200) { 145 Log.d(TAG, "Received error response id=" + result.transactionId() 146 + " code=" + result.responseCode()); 147 return Futures.immediateFailedFuture( 148 new ChatServiceException("Msrp response code: " + result.responseCode(), 149 CODE_ERROR_SEND_MESSAGE_FAILED)); 150 } 151 return Futures.immediateFuture(null); 152 }, MoreExecutors.directExecutor()); 153 } 154 155 /** Start outgoing chat session. */ 156 ListenableFuture<Void> start(String telUriContact) { 157 if (mStartFuture != null) { 158 return Futures.immediateFailedFuture( 159 new ChatServiceException("Session already started")); 160 } 161 162 SettableFuture<Void> future = SettableFuture.create(); 163 mStartFuture = future; 164 mRemoteUri = SipUtils.createUri(telUriContact); 165 try { 166 SipSessionConfiguration configuration = 167 mContext.getSipSession().getSessionConfiguration(); 168 SimpleSdpMessage sdp = SdpUtils.createSdpForMsrp(configuration.getLocalIpAddress(), 169 false); 170 SIPRequest invite = 171 SipUtils.buildInvite( 172 mContext.getSipSession().getSessionConfiguration(), 173 telUriContact, 174 mConversationId, 175 sdp.encode().getBytes(UTF_8)); 176 mInviteRequest = invite; 177 mLocalSdp = sdp; 178 Futures.addCallback( 179 mService.sendSipRequest(invite, this), 180 new FutureCallback<Boolean>() { 181 @Override 182 public void onSuccess(Boolean result) { 183 Log.i(TAG, "onSuccess:" + result); 184 if (!result) { 185 notifyFailure("Failed to send INVITE", CODE_ERROR_UNSPECIFIED); 186 } 187 } 188 189 @Override 190 public void onFailure(Throwable t) { 191 Log.i(TAG, "onFailure:" + t.getMessage()); 192 notifyFailure("Failed to send INVITE", CODE_ERROR_UNSPECIFIED); 193 } 194 }, 195 MoreExecutors.directExecutor()); 196 } catch (ParseException e) { 197 Log.e(TAG, e.getMessage()); 198 e.printStackTrace(); 199 return Futures.immediateFailedFuture( 200 new ChatServiceException("Failed to build INVITE")); 201 } 202 203 return future; 204 } 205 206 /** Start incoming chat session. */ 207 ListenableFuture<Void> start(SIPRequest invite) { 208 mInviteRequest = invite; 209 int statusCode = Response.OK; 210 if (!SipUtils.hasSdpContent(invite)) { 211 statusCode = Response.NOT_ACCEPTABLE_HERE; 212 } else { 213 try { 214 mRemoteSdp = SimpleSdpMessage.parse( 215 new ByteArrayInputStream(invite.getRawContent())); 216 } catch (ParseException | IOException e) { 217 statusCode = Response.BAD_REQUEST; 218 } 219 } 220 221 updateRemoteUri(mInviteRequest); 222 223 SipSessionConfiguration configuration = mContext.getSipSession().getSessionConfiguration(); 224 SimpleSdpMessage sdp = SdpUtils.createSdpForMsrp(configuration.getLocalIpAddress(), false); 225 226 // Automatically reply back to the invite by building a pre-canned response. 227 try { 228 SIPResponse response = SipUtils.buildInviteResponse(configuration, invite, statusCode, 229 sdp); 230 mLocalSdp = sdp; 231 return Futures.transform( 232 mService.sendSipResponse(response, this), result -> null, 233 MoreExecutors.directExecutor()); 234 } catch (ParseException e) { 235 Log.e(TAG, "Exception while building response", e); 236 return Futures.immediateFailedFuture(e); 237 } 238 } 239 240 /** Terminate the current SIP session. */ 241 public ListenableFuture<Void> terminate() { 242 if (mInviteRequest == null) { 243 return Futures.immediateFuture(null); 244 } 245 try { 246 if (mMsrpSession != null) { 247 mMsrpSession.terminate(); 248 } 249 } catch (IOException e) { 250 return Futures.immediateFailedFuture( 251 new ChatServiceException( 252 "Exception while terminating MSRP session", CODE_ERROR_UNSPECIFIED)); 253 } 254 try { 255 256 SettableFuture<Void> future = SettableFuture.create(); 257 Futures.addCallback( 258 mService.sendSipRequest(SipUtils.buildBye(mInviteRequest), this), 259 new FutureCallback<Boolean>() { 260 @Override 261 public void onSuccess(Boolean result) { 262 future.set(null); 263 } 264 265 @Override 266 public void onFailure(Throwable t) { 267 future.setException( 268 new ChatServiceException("Failed to send BYE", 269 CODE_ERROR_UNSPECIFIED, t)); 270 } 271 }, 272 MoreExecutors.directExecutor()); 273 return future; 274 } catch (ParseException e) { 275 return Futures.immediateFailedFuture( 276 new ChatServiceException("Failed to build BYE", CODE_ERROR_UNSPECIFIED)); 277 } 278 } 279 280 void receiveMessage(Message msg) { 281 if (msg instanceof SIPRequest) { 282 handleSipRequest((SIPRequest) msg); 283 } else { 284 handleSipResponse((SIPResponse) msg); 285 } 286 } 287 288 private void handleSipRequest(SIPRequest request) { 289 SIPResponse response; 290 if (TextUtils.equals(request.getMethod(), Request.ACK)) { 291 // Terminating session established, start a msrp session. 292 if (mRemoteSdp != null) { 293 startMsrpSession(mRemoteSdp); 294 } 295 return; 296 } 297 298 if (TextUtils.equals(request.getMethod(), Request.BYE)) { 299 response = request.createResponse(Response.OK); 300 } else { 301 // Currently we support only INVITE and BYE. 302 response = request.createResponse(Response.METHOD_NOT_ALLOWED); 303 } 304 Futures.addCallback( 305 mService.sendSipResponse(response, this), 306 new FutureCallback<Boolean>() { 307 @Override 308 public void onSuccess(Boolean result) { 309 if (result) { 310 Log.d( 311 TAG, 312 "Response to Call-Id: " 313 + response.getCallId().getCallId() 314 + " sent successfully"); 315 } else { 316 Log.d(TAG, "Failed to send response"); 317 } 318 } 319 320 @Override 321 public void onFailure(Throwable t) { 322 Log.d(TAG, "Exception while sending response: ", t); 323 } 324 }, 325 MoreExecutors.directExecutor()); 326 } 327 328 private void handleSipResponse(SIPResponse response) { 329 int code = response.getStatusCode(); 330 331 // Nothing to do for a provisional response. 332 if (response.isFinalResponse()) { 333 if (code == Response.OK) { 334 handle200OK(response); 335 } else { 336 handleNon200(response); 337 } 338 } 339 } 340 341 private void handleNon200(SIPResponse response) { 342 Log.d(TAG, "Received error response code=" + response.getStatusCode()); 343 notifyFailure("Received non-200 INVITE response", CODE_ERROR_UNSPECIFIED); 344 } 345 346 private void handle200OK(SIPResponse response) { 347 if (!SipUtils.hasSdpContent(response)) { 348 notifyFailure("Content is not a SDP", CODE_ERROR_UNSPECIFIED); 349 return; 350 } 351 352 SimpleSdpMessage sdp; 353 try { 354 sdp = SimpleSdpMessage.parse(new ByteArrayInputStream(response.getRawContent())); 355 } catch (ParseException | IOException e) { 356 notifyFailure("Invalid SDP in INVITE", CODE_ERROR_UNSPECIFIED); 357 return; 358 } 359 360 if (mInviteRequest == null) { 361 notifyFailure("No INVITE request sent out", CODE_ERROR_UNSPECIFIED); 362 return; 363 } 364 365 SIPRequest ack = mInviteRequest.createAckRequest((To) response.getToHeader()); 366 Futures.addCallback( 367 mService.sendSipRequest(ack, this), 368 new FutureCallback<Boolean>() { 369 @Override 370 public void onSuccess(Boolean result) { 371 if (result) { 372 startMsrpSession(sdp); 373 mRemoteSdp = sdp; 374 } else { 375 notifyFailure("Failed to send ACK", CODE_ERROR_UNSPECIFIED); 376 } 377 } 378 379 @Override 380 public void onFailure(Throwable t) { 381 notifyFailure("Failed to send ACK", CODE_ERROR_UNSPECIFIED); 382 } 383 }, 384 MoreExecutors.directExecutor()); 385 } 386 387 private void notifyFailure(String message, @ErrorCode int code) { 388 if (mStartFuture != null) { 389 mStartFuture.setException(new ChatServiceException(message, code)); 390 mStartFuture = null; 391 } 392 } 393 394 private void notifySuccess() { 395 if (mStartFuture != null) { 396 mStartFuture.set(null); 397 mStartFuture = null; 398 } 399 } 400 401 private void startMsrpSession(SimpleSdpMessage remoteSdp) { 402 Log.d(TAG, "Start MSRP session: " + remoteSdp); 403 if (remoteSdp.getAddress().isPresent() && remoteSdp.getPort().isPresent()) { 404 String localIp = getLocalIp(); 405 Futures.addCallback( 406 mMsrpManager.createMsrpSession( 407 remoteSdp.getAddress().get(), remoteSdp.getPort().getAsInt(), localIp, 408 0 /* localPort */, this::receiveMsrpChunk), 409 new FutureCallback<MsrpSession>() { 410 @Override 411 public void onSuccess(MsrpSession result) { 412 mMsrpSession = result; 413 sendEmptyPacket(); 414 notifySuccess(); 415 } 416 417 @Override 418 public void onFailure(Throwable t) { 419 Log.e(TAG, "Failed to create msrp session", t); 420 notifyFailure("Failed to establish msrp session", 421 CODE_ERROR_UNSPECIFIED); 422 terminate() 423 .addListener( 424 () -> Log.d(TAG, "Session terminated"), 425 MoreExecutors.directExecutor()); 426 } 427 }, 428 MoreExecutors.directExecutor()); 429 } else { 430 Log.e(TAG, "Address or port is not present"); 431 } 432 } 433 434 private void sendEmptyPacket() { 435 MsrpChunk msrpChunk = 436 MsrpChunk.newBuilder() 437 .method(MsrpChunk.Method.SEND) 438 .transactionId(MsrpUtils.generateRandomId()) 439 .continuation(Continuation.COMPLETE) 440 .addHeader(MsrpConstants.HEADER_TO_PATH, mRemoteSdp.getPath().get()) 441 .addHeader(MsrpConstants.HEADER_FROM_PATH, mLocalSdp.getPath().get()) 442 .addHeader(MsrpConstants.HEADER_FAILURE_REPORT, 443 MsrpConstants.REPORT_VALUE_NO) 444 .addHeader(MsrpConstants.HEADER_SUCCESS_REPORT, 445 MsrpConstants.REPORT_VALUE_NO) 446 .addHeader(MsrpConstants.HEADER_BYTE_RANGE, "1/0-0") 447 .addHeader(MsrpConstants.HEADER_MESSAGE_ID, MsrpUtils.generateRandomId()) 448 .build(); 449 450 mMsrpSession.send(msrpChunk); 451 } 452 453 private String getLocalIp() { 454 SipSessionConfiguration configuration = mContext.getSipSession().getSessionConfiguration(); 455 return configuration.getLocalIpAddress(); 456 } 457 458 private void receiveMsrpChunk(MsrpChunk chunk) { 459 Log.d(TAG, "Received msrp= " + chunk + " conversation=" + mConversationId); 460 461 MsrpChunkHeader contentTypeHeader = chunk.header("Content-Type"); 462 if (chunk.content().length == 0 || contentTypeHeader == null) { 463 Log.i(TAG, "No content or Content-Type header, drop it"); 464 return; 465 } 466 467 String contentType = contentTypeHeader.value(); 468 if ("message/cpim".equals(contentType)) { 469 Log.d(TAG, "Received CPIM: " + new String(chunk.content(), UTF_8)); 470 try { 471 SimpleCpimMessage cpim = SimpleCpimMessage.parse(chunk.content()); 472 if (mListener != null) { 473 mListener.onMessageReceived(cpim); 474 } 475 } catch (Exception e) { 476 Log.e(TAG, "Error while parsing cpim message.", e); 477 } 478 } else { 479 Log.w(TAG, contentType + " is not supported."); 480 } 481 } 482 483 484 /** Set new listener for this session. */ 485 public void setListener(@Nullable ChatSessionListener listener) { 486 mListener = listener; 487 } 488 489 private void updateRemoteUri(SIPRequest request) { 490 PAssertedIdentityHeader pAssertedIdentityHeader = 491 (PAssertedIdentityHeader) request.getHeader("P-Asserted-Identity"); 492 if (pAssertedIdentityHeader == null) { 493 mRemoteUri = request.getFrom().getAddress().getURI(); 494 } else { 495 mRemoteUri = pAssertedIdentityHeader.getAddress().getURI(); 496 } 497 } 498 } 499