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 android.content.Context; 20 import android.telephony.ims.SipDelegateConnection; 21 import android.text.TextUtils; 22 import android.util.Log; 23 24 import androidx.annotation.Nullable; 25 26 import com.android.libraries.rcs.simpleclient.SimpleRcsClientContext; 27 import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpManager; 28 import com.android.libraries.rcs.simpleclient.protocol.sip.SipSession; 29 import com.android.libraries.rcs.simpleclient.protocol.sip.SipSessionListener; 30 import com.android.libraries.rcs.simpleclient.protocol.sip.SipUtils; 31 import com.android.libraries.rcs.simpleclient.service.ImsService; 32 import com.android.libraries.rcs.simpleclient.service.StateChangeCallback; 33 34 import com.google.common.collect.ImmutableSet; 35 import com.google.common.util.concurrent.Futures; 36 import com.google.common.util.concurrent.ListenableFuture; 37 import com.google.common.util.concurrent.MoreExecutors; 38 39 import gov.nist.javax.sip.message.SIPRequest; 40 import gov.nist.javax.sip.message.SIPResponse; 41 42 import java.text.ParseException; 43 import java.util.HashMap; 44 import java.util.Map; 45 import java.util.Set; 46 47 import javax.sip.message.Request; 48 import javax.sip.message.Response; 49 50 /** 51 * Minimal CPM chat session service that provides the interface creating a {@link SimpleChatSession} 52 * instance using {@link SipDelegateConnection}. 53 */ 54 public class MinimalCpmChatService implements ImsService { 55 public static final String CPM_SESSION_TAG = 56 "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.oma.cpm.session\""; 57 private static final String TAG = MinimalCpmChatService.class.getSimpleName(); 58 private final Map<String, SimpleChatSession> mTransactions = new HashMap<>(); 59 private final Map<String, SimpleChatSession> mDialogs = new HashMap<>(); 60 61 private final MsrpManager mMsrpManager; 62 private SimpleRcsClientContext mContext; 63 64 @Nullable 65 private ChatServiceListener mListener; 66 67 private final SipSessionListener mSipSessionListener = 68 sipMessage -> { 69 if (sipMessage instanceof SIPRequest) { 70 handleRequest((SIPRequest) sipMessage); 71 } else if (sipMessage instanceof SIPResponse) { 72 handleResponse((SIPResponse) sipMessage); 73 } 74 }; 75 MinimalCpmChatService(Context context)76 public MinimalCpmChatService(Context context) { 77 mMsrpManager = new MsrpManager(context); 78 } 79 80 @Override getFeatureTags()81 public Set<String> getFeatureTags() { 82 return ImmutableSet.of(CPM_SESSION_TAG); 83 } 84 85 @Override start(SimpleRcsClientContext context)86 public void start(SimpleRcsClientContext context) { 87 mContext = context; 88 context.getSipSession().setSessionListener(mSipSessionListener); 89 } 90 91 @Override stop()92 public void stop() { 93 } 94 95 @Override onStateChange(StateChangeCallback cb)96 public void onStateChange(StateChangeCallback cb) { 97 } 98 99 /** 100 * Start an originating 1:1 chat session interacting with the RCS server. 101 * 102 * @param telUriContact The remote contact in the from of TEL URI 103 * @return The future will be completed with SimpleChatSession once the session is established 104 * successfully. If the session fails for any reason, return the failed future with {@link 105 * ChatServiceException} 106 */ startOriginatingChatSession(String telUriContact)107 public ListenableFuture<SimpleChatSession> startOriginatingChatSession(String telUriContact) { 108 Log.i(TAG, "startOriginatingChatSession"); 109 SimpleChatSession session = new SimpleChatSession(mContext, this, mMsrpManager); 110 return Futures.transform( 111 session.start(telUriContact), v -> session, MoreExecutors.directExecutor()); 112 } 113 sendSipRequest(SIPRequest msg, SimpleChatSession session)114 ListenableFuture<Boolean> sendSipRequest(SIPRequest msg, SimpleChatSession session) { 115 Log.i(TAG, "sendSipRequest:\r\n" + msg); 116 if (!TextUtils.equals(msg.getMethod(), Request.ACK)) { 117 mTransactions.put(msg.getTransactionId(), session); 118 } 119 120 if (TextUtils.equals(msg.getMethod(), Request.BYE)) { 121 mDialogs.remove(msg.getDialogId(/* isServer= */ false)); 122 } 123 124 SipSession sipSession = mContext.getSipSession(); 125 return sipSession.send(msg); 126 } 127 sendSipResponse(SIPResponse msg, SimpleChatSession session)128 ListenableFuture<Boolean> sendSipResponse(SIPResponse msg, SimpleChatSession session) { 129 Log.i(TAG, "sendSipResponse:\r\n" + msg); 130 if (TextUtils.equals(msg.getCSeq().getMethod(), Request.BYE)) { 131 mDialogs.remove(msg.getDialogId(/* isServer= */ true)); 132 } else if (TextUtils.equals(msg.getCSeq().getMethod(), Request.INVITE) 133 && msg.getStatusCode() == Response.OK) { 134 // Cache the dialog in order to route in-dialog request to the corresponding session. 135 mDialogs.put(msg.getDialogId(/* isServer= */ true), session); 136 } 137 SipSession sipSession = mContext.getSipSession(); 138 return sipSession.send(msg); 139 } 140 handleRequest(SIPRequest request)141 private void handleRequest(SIPRequest request) { 142 Log.i(TAG, "handleRequest:\r\n" + request); 143 String dialogId = request.getDialogId(/* isServer= */ true); 144 if (mDialogs.containsKey(dialogId)) { 145 SimpleChatSession session = mDialogs.get(dialogId); 146 session.receiveMessage(request); 147 } else if (TextUtils.equals(request.getMethod(), Request.INVITE)) { 148 SimpleChatSession session = new SimpleChatSession(mContext, this, mMsrpManager); 149 session 150 .start(request) 151 .addListener( 152 () -> { 153 ChatServiceListener listener = mListener; 154 if (listener != null) { 155 listener.onIncomingSession(session); 156 } 157 }, 158 MoreExecutors.directExecutor()); 159 } else { 160 // Reject non-INVITE request. 161 try { 162 SIPResponse response = 163 SipUtils.buildInviteResponse( 164 mContext.getSipSession().getSessionConfiguration(), 165 request, 166 Response.METHOD_NOT_ALLOWED, 167 null); 168 sendSipResponse(response, /* session= */ null) 169 .addListener(() -> { 170 }, MoreExecutors.directExecutor()); 171 } catch (ParseException e) { 172 Log.e(TAG, "Exception while sending response", e); 173 } 174 } 175 } 176 handleResponse(SIPResponse response)177 private void handleResponse(SIPResponse response) { 178 Log.i(TAG, "handleResponse:\r\n" + response); 179 // catch the exception because abnormal response always causes App to crash. 180 try { 181 SimpleChatSession session = mTransactions.get(response.getTransactionId()); 182 if (session != null) { 183 if (response.isFinalResponse()) { 184 mTransactions.remove(response.getTransactionId()); 185 186 // Cache the dialog in order to route in-dialog request to the corresponding 187 // session. 188 if (TextUtils.equals(response.getCSeq().getMethod(), Request.INVITE) 189 && response.getStatusCode() == Response.OK) { 190 mDialogs.put(response.getDialogId(/* isServer= */ false), session); 191 } 192 } 193 194 session.receiveMessage(response); 195 } 196 } catch (Exception e) { 197 Log.e(TAG, e.getMessage()); 198 e.printStackTrace(); 199 } 200 } 201 202 /** Set new listener for the chat service. */ setListener(@ullable ChatServiceListener listener)203 public void setListener(@Nullable ChatServiceListener listener) { 204 mListener = listener; 205 } 206 } 207