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