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.google.android.sample.rcsclient.util;
18 
19 import android.content.ContentValues;
20 import android.content.Context;
21 import android.database.Cursor;
22 import android.net.Uri;
23 import android.telephony.ims.ImsManager;
24 import android.text.TextUtils;
25 import android.util.Log;
26 
27 import com.android.libraries.rcs.simpleclient.SimpleRcsClient;
28 import com.android.libraries.rcs.simpleclient.SimpleRcsClient.State;
29 import com.android.libraries.rcs.simpleclient.provisioning.ProvisioningController;
30 import com.android.libraries.rcs.simpleclient.provisioning.StaticConfigProvisioningController;
31 import com.android.libraries.rcs.simpleclient.registration.RegistrationController;
32 import com.android.libraries.rcs.simpleclient.registration.RegistrationControllerImpl;
33 import com.android.libraries.rcs.simpleclient.service.chat.MinimalCpmChatService;
34 import com.android.libraries.rcs.simpleclient.service.chat.SimpleChatSession;
35 
36 import com.google.android.sample.rcsclient.RcsStateChangedCallback;
37 import com.google.android.sample.rcsclient.SessionStateCallback;
38 import com.google.common.util.concurrent.FutureCallback;
39 import com.google.common.util.concurrent.Futures;
40 import com.google.common.util.concurrent.ListenableFuture;
41 import com.google.common.util.concurrent.MoreExecutors;
42 
43 import gov.nist.javax.sip.address.AddressFactoryImpl;
44 
45 import java.text.ParseException;
46 import java.time.Instant;
47 import java.util.HashMap;
48 import java.util.concurrent.ExecutorService;
49 import java.util.concurrent.Executors;
50 
51 import javax.sip.address.AddressFactory;
52 import javax.sip.address.URI;
53 
54 /**
55  * This class takes advantage of rcs library to manage chat session and send/receive chat message.
56  */
57 public class ChatManager {
58     public static final String SELF = "self";
59     private static final String TAG = "TestRcsApp.ChatManager";
60     private static final String TELURI_PREFIX = "tel:";
61     private static AddressFactory sAddressFactory = new AddressFactoryImpl();
62     private static HashMap<Integer, ChatManager> sChatManagerInstances = new HashMap<>();
63     private final ExecutorService mFixedThreadPool = Executors.newFixedThreadPool(5);
64     private Context mContext;
65     private ProvisioningController mProvisioningController;
66     private RegistrationController mRegistrationController;
67     private MinimalCpmChatService mImsService;
68     private SimpleRcsClient mSimpleRcsClient;
69     private State mState;
70     private int mSubId;
71     private HashMap<String, SimpleChatSession> mContactSessionMap = new HashMap<>();
72     private RcsStateChangedCallback mRcsStateChangedCallback;
73 
ChatManager(Context context, int subId)74     private ChatManager(Context context, int subId) {
75         mContext = context;
76         mSubId = subId;
77         mProvisioningController = StaticConfigProvisioningController.createForSubscriptionId(subId,
78                 context);
79         ImsManager imsManager = mContext.getSystemService(ImsManager.class);
80         mRegistrationController = new RegistrationControllerImpl(subId, mFixedThreadPool,
81                 imsManager);
82         mImsService = new MinimalCpmChatService(context);
83         mSimpleRcsClient = SimpleRcsClient.newBuilder()
84                 .registrationController(mRegistrationController)
85                 .provisioningController(mProvisioningController)
86                 .imsService(mImsService).build();
87 
88         mState = State.NEW;
89         // register callback for state change
90         mSimpleRcsClient.onStateChanged((oldState, newState) -> {
91             Log.i(TAG, "notifyStateChange() oldState:" + oldState + " newState:" + newState);
92             mState = newState;
93             mRcsStateChangedCallback.notifyStateChange(oldState, newState);
94         });
95         mImsService.setListener((session) -> {
96             Log.i(TAG, "onIncomingSession():" + session.getRemoteUri());
97             String phoneNumber = getNumberFromUri(session.getRemoteUri().toString());
98             mContactSessionMap.put(phoneNumber, session);
99             session.setListener(
100                     // implement onMessageReceived()
101                     (message) -> {
102                         mFixedThreadPool.execute(() -> {
103                             String msg = message.content();
104                             if (TextUtils.isEmpty(phoneNumber)) {
105                                 Log.i(TAG, "dest number is empty, uri:"
106                                         + session.getRemoteUri());
107                             } else {
108                                 addNewMessage(msg, phoneNumber, SELF);
109                             }
110                         });
111                     });
112         });
113     }
114 
115     /**
116      * Create ChatManager with a specific subId.
117      */
getInstance(Context context, int subId)118     public static ChatManager getInstance(Context context, int subId) {
119         synchronized (sChatManagerInstances) {
120             if (sChatManagerInstances.containsKey(subId)) {
121                 return sChatManagerInstances.get(subId);
122             }
123             ChatManager chatManager = new ChatManager(context, subId);
124             sChatManagerInstances.put(subId, chatManager);
125             return chatManager;
126         }
127     }
128 
129     /**
130      * Try to parse the given uri.
131      *
132      * @throws IllegalArgumentException in case of parsing error.
133      */
createUri(String uri)134     public static URI createUri(String uri) {
135         try {
136             return sAddressFactory.createURI(uri);
137         } catch (ParseException exception) {
138             throw new IllegalArgumentException("URI cannot be created", exception);
139         }
140     }
141 
getNumberFromUri(String number)142     private static String getNumberFromUri(String number) {
143         String[] numberParts = number.split("[@;:]");
144         if (numberParts.length < 2) {
145             return null;
146         }
147         return numberParts[1];
148     }
149 
150     /**
151      * set callback for RCS state change.
152      */
setRcsStateChangedCallback(RcsStateChangedCallback callback)153     public void setRcsStateChangedCallback(RcsStateChangedCallback callback) {
154         mRcsStateChangedCallback = callback;
155     }
156 
157     /**
158      * Start to register by doing provisioning and creating SipDelegate
159      */
register()160     public void register() {
161         Log.i(TAG, "do start(), State State = " + mState);
162         if (mState == State.NEW) {
163             mSimpleRcsClient.start();
164         }
165     }
166 
167     /**
168      * Deregister chat feature.
169      */
deregister()170     public void deregister() {
171         Log.i(TAG, "deregister");
172         sChatManagerInstances.remove(mSubId);
173         mSimpleRcsClient.stop();
174     }
175 
176     /**
177      * Initiate 1 to 1 chat session.
178      *
179      * @param contact  destination phone number.
180      * @param callback callback for session state.
181      */
initChatSession(String contact, SessionStateCallback callback)182     public void initChatSession(String contact, SessionStateCallback callback) {
183         if (mState != State.REGISTERED) {
184             Log.i(TAG, "Could not init session due to State = " + mState);
185             return;
186         }
187         Log.i(TAG, "initChatSession contact: " + contact);
188         if (mContactSessionMap.containsKey(contact)) {
189             callback.onSuccess();
190             Log.i(TAG, "contact exists");
191             return;
192         }
193         Futures.addCallback(
194                 mImsService.startOriginatingChatSession(TELURI_PREFIX + contact),
195                 new FutureCallback<SimpleChatSession>() {
196                     @Override
197                     public void onSuccess(SimpleChatSession chatSession) {
198                         String phoneNumber = getNumberFromUri(
199                                 chatSession.getRemoteUri().toString());
200                         mContactSessionMap.put(phoneNumber, chatSession);
201                         chatSession.setListener(
202                                 // implement onMessageReceived()
203                                 (message) -> {
204                                     mFixedThreadPool.execute(() -> {
205                                         String msg = message.content();
206                                         if (TextUtils.isEmpty(phoneNumber)) {
207                                             Log.i(TAG, "dest number is empty, uri:"
208                                                     + chatSession.getRemoteUri());
209                                         } else {
210                                             addNewMessage(msg, phoneNumber, SELF);
211                                         }
212                                     });
213 
214                                 });
215                         callback.onSuccess();
216                     }
217 
218                     @Override
219                     public void onFailure(Throwable t) {
220                         callback.onFailure();
221                     }
222                 },
223                 MoreExecutors.directExecutor());
224     }
225 
226     /**
227      * Send a chat message.
228      *
229      * @param contact destination phone number.
230      * @param message chat message.
231      */
sendMessage(String contact, String message)232     public ListenableFuture<Void> sendMessage(String contact, String message) {
233         SimpleChatSession chatSession = mContactSessionMap.get(contact);
234         if (chatSession == null) {
235             Log.i(TAG, "session is unavailable for contact = " + contact);
236             return Futures.immediateFailedFuture(
237                     new IllegalStateException("Chat session does not exist"));
238         }
239         return chatSession.sendMessage(message);
240     }
241 
isRegistered()242     public boolean isRegistered() {
243         return (mState == State.REGISTERED);
244     }
245 
246     /**
247      * Terminate the chat session.
248      *
249      * @param contact destination phone number.
250      */
terminateSession(String contact)251     public void terminateSession(String contact) {
252         Log.i(TAG, "terminateSession");
253         SimpleChatSession chatSession = mContactSessionMap.get(contact);
254         if (chatSession == null) {
255             Log.i(TAG, "session is unavailable for contact = " + contact);
256             return;
257         }
258         chatSession.terminate();
259         mContactSessionMap.remove(contact);
260     }
261 
262     /**
263      * Insert chat information into database.
264      *
265      * @param message chat message.
266      * @param src source phone number.
267      * @param dest destination phone number.
268      */
addNewMessage(String message, String src, String dest)269     public Uri addNewMessage(String message, String src, String dest) {
270         long currentTime = Instant.now().getEpochSecond();
271         ContentValues contentValues = new ContentValues();
272         contentValues.put(ChatProvider.RcsColumns.SRC_PHONE_NUMBER, src);
273         contentValues.put(ChatProvider.RcsColumns.DEST_PHONE_NUMBER, dest);
274         contentValues.put(ChatProvider.RcsColumns.CHAT_MESSAGE, message);
275         contentValues.put(ChatProvider.RcsColumns.MSG_TIMESTAMP, currentTime);
276         contentValues.put(ChatProvider.RcsColumns.IS_READ, Boolean.TRUE);
277         // insert chat table
278         Uri result = mContext.getContentResolver().insert(ChatProvider.CHAT_URI, contentValues);
279 
280         ContentValues summary = new ContentValues();
281         summary.put(ChatProvider.SummaryColumns.LATEST_MESSAGE, message);
282         summary.put(ChatProvider.SummaryColumns.MSG_TIMESTAMP, currentTime);
283         summary.put(ChatProvider.SummaryColumns.IS_READ, Boolean.TRUE);
284 
285         String remoteNumber = src.equals(SELF) ? dest : src;
286         if (remoteNumberExists(remoteNumber)) {
287             mContext.getContentResolver().update(ChatProvider.SUMMARY_URI, summary,
288                     ChatProvider.SummaryColumns.REMOTE_PHONE_NUMBER + "=?",
289                     new String[]{remoteNumber});
290         } else {
291             summary.put(ChatProvider.SummaryColumns.REMOTE_PHONE_NUMBER, remoteNumber);
292             mContext.getContentResolver().insert(ChatProvider.SUMMARY_URI, summary);
293         }
294         return result;
295     }
296 
297     /**
298      * Update MSRP chat message sent result.
299      */
updateMsgResult(String id, boolean success)300     public void updateMsgResult(String id, boolean success) {
301         ContentValues contentValues = new ContentValues();
302         contentValues.put(ChatProvider.RcsColumns.RESULT, success);
303         mContext.getContentResolver().update(ChatProvider.CHAT_URI, contentValues,
304                 ChatProvider.RcsColumns._ID + "=?", new String[]{id});
305     }
306 
307     /**
308      * Check if the number exists in the database.
309      */
remoteNumberExists(String number)310     public boolean remoteNumberExists(String number) {
311         Cursor cursor = mContext.getContentResolver().query(ChatProvider.SUMMARY_URI, null,
312                 ChatProvider.SummaryColumns.REMOTE_PHONE_NUMBER + "=?", new String[]{number},
313                 null);
314         if (cursor != null) {
315             int count = cursor.getCount();
316             return count > 0;
317         }
318         return false;
319     }
320 
321 }
322