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