1 /* 2 * Copyright (C) 2021 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.services.telephony.rcs; 18 19 import android.telephony.ims.SipDialogState; 20 import android.telephony.ims.SipMessage; 21 import android.util.ArrayMap; 22 import android.util.ArraySet; 23 import android.util.LocalLog; 24 import android.util.Log; 25 26 import com.android.internal.annotations.VisibleForTesting; 27 import com.android.internal.telephony.SipMessageParsingUtils; 28 import com.android.internal.telephony.metrics.RcsStats; 29 import com.android.internal.util.IndentingPrintWriter; 30 31 import java.io.PrintWriter; 32 import java.util.ArrayList; 33 import java.util.Arrays; 34 import java.util.Collections; 35 import java.util.List; 36 import java.util.Set; 37 import java.util.UUID; 38 import java.util.stream.Collectors; 39 40 /** 41 * Tracks the state of SIP sessions started by a SIP INVITE (see RFC 3261) 42 * <p> 43 * Each SIP session created will consist of one or more SIP with, each dialog in the session 44 * having the same call-ID. Each SIP dialog will be in one of three states: EARLY, CONFIRMED, and 45 * CLOSED. 46 * <p> 47 * The SIP session will be closed once all of the associated dialogs are closed. 48 */ 49 public class SipSessionTracker { 50 private static final String TAG = "SessionT"; 51 52 /** 53 * SIP request methods that will start a new SIP Dialog and move it into the PENDING state 54 * while we wait for a response. Note: INVITE is not the only SIP dialog that will create a 55 * dialog, however it is the only one that we wish to track for this use case. 56 */ 57 public static final String[] SIP_REQUEST_DIALOG_START_METHODS = new String[] { "invite" }; 58 59 /** 60 * The SIP request method that will close a SIP Dialog in the ACTIVE state with the same 61 * Call-Id. 62 */ 63 private static final String SIP_CLOSE_DIALOG_REQUEST_METHOD = "bye"; 64 65 private final LocalLog mLocalLog = new LocalLog(SipTransportController.LOG_SIZE); 66 private final ArrayList<SipDialog> mTrackedDialogs = new ArrayList<>(); 67 // Operations that are pending an ack from the remote application processing the message before 68 // they can be applied here. Maps the via header branch parameter of the message to the 69 // associated pending operation. 70 private final ArrayMap<String, Runnable> mPendingAck = new ArrayMap<>(); 71 72 private final RcsStats mRcsStats; 73 int mSubId; 74 private SipDialogsStateListener mSipDialogsListener; 75 private String mDelegateKey; 76 SipSessionTracker(int subId, RcsStats rcsStats)77 public SipSessionTracker(int subId, RcsStats rcsStats) { 78 mSubId = subId; 79 mRcsStats = rcsStats; 80 mDelegateKey = String.valueOf(UUID.randomUUID()); 81 } 82 83 /** 84 * Filter a SIP message to determine if it will result in a new SIP dialog. This will need to be 85 * successfully acknowledged by the remote IMS stack using 86 * {@link #acknowledgePendingMessage(String)} before we do any further processing. 87 * 88 * @param message The Incoming SIP message. 89 */ filterSipMessage(int direction, SipMessage message)90 public void filterSipMessage(int direction, SipMessage message) { 91 final Runnable r; 92 if (startsEarlyDialog(message)) { 93 r = getCreateDialogRunnable(direction, message); 94 } else if (closesDialog(message)) { 95 r = getCloseDialogRunnable(message); 96 } else if (SipMessageParsingUtils.isSipResponse(message.getStartLine())) { 97 r = getDialogStateChangeRunnable(message); 98 } else { 99 r = null; 100 } 101 102 if (r != null) { 103 if (mPendingAck.containsKey(message.getViaBranchParameter())) { 104 Runnable lastEvent = mPendingAck.get(message.getViaBranchParameter()); 105 logw("Adding new message when there was already a pending event for branch: " 106 + message.getViaBranchParameter()); 107 Runnable concatRunnable = () -> { 108 // No choice but to concatenate the Runnables together. 109 if (lastEvent != null) lastEvent.run(); 110 r.run(); 111 }; 112 mPendingAck.put(message.getViaBranchParameter(), concatRunnable); 113 } else { 114 mPendingAck.put(message.getViaBranchParameter(), r); 115 } 116 } 117 } 118 119 /** 120 * The pending SIP message has been received by the remote IMS stack. We can now track dialogs 121 * associated with this message. 122 * message. 123 * @param viaBranchId The SIP message's Via header's branch parameter, which is used as a 124 * unique token. 125 */ acknowledgePendingMessage(String viaBranchId)126 public void acknowledgePendingMessage(String viaBranchId) { 127 Runnable r = mPendingAck.get(viaBranchId); 128 if (r != null) { 129 mPendingAck.remove(viaBranchId); 130 r.run(); 131 } 132 } 133 134 /** 135 * The pending SIP message has failed to be sent to the remote so remove the pending task. 136 * @param viaBranchId The failed message's Via header's branch parameter. 137 */ pendingMessageFailed(String viaBranchId)138 public void pendingMessageFailed(String viaBranchId) { 139 mPendingAck.remove(viaBranchId); 140 } 141 142 /** 143 * A SIP session tracked by the remote application's IMS stack has been closed, so we can stop 144 * tracking it. 145 * @param callId The callId of the SIP session that has been closed. 146 */ cleanupSession(String callId)147 public void cleanupSession(String callId) { 148 List<SipDialog> dialogsToCleanup = mTrackedDialogs.stream() 149 .filter(d -> d.getCallId().equals(callId)) 150 .collect(Collectors.toList()); 151 if (dialogsToCleanup.isEmpty()) return; 152 logi("Cleanup dialogs associated with call id: " + callId); 153 for (SipDialog d : dialogsToCleanup) { 154 mRcsStats.onSipTransportSessionClosed(mSubId, callId, 0, 155 d.getState() == d.STATE_CLOSED); 156 d.close(); 157 logi("Dialog closed: " + d); 158 } 159 mTrackedDialogs.removeAll(dialogsToCleanup); 160 notifySipDialogState(); 161 } 162 163 /** 164 * @return the call IDs of the dialogs associated with the provided feature tags. 165 */ getCallIdsAssociatedWithFeatureTag(Set<String> featureTags)166 public Set<String> getCallIdsAssociatedWithFeatureTag(Set<String> featureTags) { 167 if (featureTags.isEmpty()) return Collections.emptySet(); 168 Set<String> associatedIds = new ArraySet<>(); 169 for (String featureTag : featureTags) { 170 for (SipDialog dialog : mTrackedDialogs) { 171 boolean isAssociated = dialog.getAcceptContactFeatureTags().stream().anyMatch( 172 d -> d.equalsIgnoreCase(featureTag)); 173 if (isAssociated) associatedIds.add(dialog.getCallId()); 174 } 175 } 176 return associatedIds; 177 } 178 179 /** 180 * @return All dialogs that have not received a final response yet 2XX or 3XX+. 181 */ getEarlyDialogs()182 public Set<SipDialog> getEarlyDialogs() { 183 return mTrackedDialogs.stream().filter(d -> d.getState() == SipDialog.STATE_EARLY) 184 .collect(Collectors.toSet()); 185 } 186 187 /** 188 * @return All confirmed dialogs that have received a 2XX response and are active. 189 */ getConfirmedDialogs()190 public Set<SipDialog> getConfirmedDialogs() { 191 return mTrackedDialogs.stream().filter(d -> d.getState() == SipDialog.STATE_CONFIRMED) 192 .collect(Collectors.toSet()); 193 } 194 195 /** 196 * @return Dialogs that have been closed via a BYE or 3XX+ response and 197 * {@link #cleanupSession(String)} has not been called yet. 198 */ 199 @VisibleForTesting getClosedDialogs()200 public Set<SipDialog> getClosedDialogs() { 201 return mTrackedDialogs.stream().filter(d -> d.getState() == SipDialog.STATE_CLOSED) 202 .collect(Collectors.toSet()); 203 } 204 205 /** 206 * @return All of the tracked dialogs, even the ones that have been closed but 207 * {@link #cleanupSession(String)} has not been called. 208 */ getTrackedDialogs()209 public Set<SipDialog> getTrackedDialogs() { 210 return new ArraySet<>(mTrackedDialogs); 211 } 212 213 /** 214 * Clears all tracked sessions. 215 */ clearAllSessions()216 public void clearAllSessions() { 217 for (SipDialog d : mTrackedDialogs) { 218 mRcsStats.onSipTransportSessionClosed(mSubId, d.getCallId(), 0, false); 219 } 220 mTrackedDialogs.clear(); 221 mPendingAck.clear(); 222 notifySipDialogState(); 223 } 224 225 /** 226 * Dump the state of this tracker to the provided PrintWriter. 227 */ dump(PrintWriter printWriter)228 public void dump(PrintWriter printWriter) { 229 IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, " "); 230 pw.println("SipSessionTracker:"); 231 pw.increaseIndent(); 232 pw.print("Early Call IDs: "); 233 pw.println(getEarlyDialogs().stream().map(SipDialog::getCallId) 234 .collect(Collectors.toSet())); 235 pw.print("Confirmed Call IDs: "); 236 pw.println(getConfirmedDialogs().stream().map(SipDialog::getCallId) 237 .collect(Collectors.toSet())); 238 pw.print("Closed Call IDs: "); 239 pw.println(getClosedDialogs().stream().map(SipDialog::getCallId) 240 .collect(Collectors.toSet())); 241 pw.println("Tracked Dialogs:"); 242 pw.increaseIndent(); 243 for (SipDialog d : mTrackedDialogs) { 244 pw.println(d); 245 } 246 pw.decreaseIndent(); 247 pw.println(); 248 pw.println("Local Logs"); 249 mLocalLog.dump(pw); 250 pw.decreaseIndent(); 251 } 252 253 /** 254 * @return {@code true}, if the SipMessage passed in should start a new SIP dialog, 255 * {@code false} if it should not. 256 */ startsEarlyDialog(SipMessage m)257 private boolean startsEarlyDialog(SipMessage m) { 258 if (!SipMessageParsingUtils.isSipRequest(m.getStartLine())) { 259 return false; 260 } 261 String[] startLineSegments = SipMessageParsingUtils.splitStartLineAndVerify( 262 m.getStartLine()); 263 if (startLineSegments == null) { 264 return false; 265 } 266 return Arrays.stream(SIP_REQUEST_DIALOG_START_METHODS) 267 .anyMatch(r -> r.equalsIgnoreCase(startLineSegments[0])); 268 } 269 270 /** 271 * @return {@code true}, if the SipMessage passed in should close a confirmed dialog, 272 * {@code false} if it should not. 273 */ closesDialog(SipMessage m)274 private boolean closesDialog(SipMessage m) { 275 if (!SipMessageParsingUtils.isSipRequest(m.getStartLine())) { 276 return false; 277 } 278 String[] startLineSegments = SipMessageParsingUtils.splitStartLineAndVerify( 279 m.getStartLine()); 280 if (startLineSegments == null) { 281 return false; 282 } 283 return SIP_CLOSE_DIALOG_REQUEST_METHOD.equalsIgnoreCase(startLineSegments[0]); 284 } 285 getCreateDialogRunnable(int direction, SipMessage m)286 private Runnable getCreateDialogRunnable(int direction, SipMessage m) { 287 return () -> { 288 List<SipDialog> duplicateDialogs = mTrackedDialogs.stream() 289 .filter(d -> d.getCallId().equals(m.getCallIdParameter())) 290 .collect(Collectors.toList()); 291 if (duplicateDialogs.size() > 0) { 292 logi("trying to create a dialog for a call ID that already exists, skip: " 293 + duplicateDialogs); 294 return; 295 } 296 SipDialog dialog = SipDialog.fromSipMessage(m); 297 String[] startLineSegments = 298 SipMessageParsingUtils.splitStartLineAndVerify(m.getStartLine()); 299 mRcsStats.earlySipTransportSession(startLineSegments[0], dialog.getCallId(), 300 direction); 301 logi("Starting new SipDialog: " + dialog); 302 mTrackedDialogs.add(dialog); 303 }; 304 } 305 getCloseDialogRunnable(SipMessage m)306 private Runnable getCloseDialogRunnable(SipMessage m) { 307 return () -> { 308 List<SipDialog> dialogsToClose = mTrackedDialogs.stream() 309 .filter(d -> d.isRequestAssociatedWithDialog(m)) 310 .collect(Collectors.toList()); 311 if (dialogsToClose.isEmpty()) return; 312 logi("Closing dialogs associated with: " + m); 313 mRcsStats.onSipTransportSessionClosed(mSubId, m.getCallIdParameter(), 0, true); 314 for (SipDialog d : dialogsToClose) { 315 d.close(); 316 logi("Dialog closed: " + d); 317 } 318 notifySipDialogState(); 319 }; 320 } 321 322 private Runnable getDialogStateChangeRunnable(SipMessage m) { 323 return () -> { 324 // This will return a dialog and all of its potential forks 325 List<SipDialog> associatedDialogs = mTrackedDialogs.stream() 326 .filter(d -> d.isResponseAssociatedWithDialog(m)) 327 .collect(Collectors.toList()); 328 if (associatedDialogs.isEmpty()) return; 329 String messageToTag = SipMessageParsingUtils.getToTag(m.getHeaderSection()); 330 // If the to tag matches (or message to tag doesn't exist in dialog yet because this is 331 // the first response), then we are done. 332 SipDialog match = associatedDialogs.stream() 333 .filter(d -> d.getToTag() == null || d.getToTag().equals(messageToTag)) 334 .findFirst().orElse(null); 335 if (match == null) { 336 // If it doesn't then we have a situation where we need to fork the existing dialog. 337 // The dialog used to fork doesn't matter, since the required params are the same, 338 // so simply use the first one in the returned list. 339 logi("Dialog forked"); 340 match = associatedDialogs.get(0).forkDialog(); 341 mTrackedDialogs.add(match); 342 } 343 if (match != null) { 344 logi("Dialog: " + match + " is associated with: " + m); 345 updateSipDialogState(match, m); 346 logi("Dialog state updated to " + match); 347 } else { 348 logi("No Dialogs are associated with: " + m); 349 } 350 }; 351 } 352 353 private void updateSipDialogState(SipDialog d, SipMessage m) { 354 String[] startLineSegments = SipMessageParsingUtils.splitStartLineAndVerify( 355 m.getStartLine()); 356 if (startLineSegments == null) { 357 logw("Could not parse start line for SIP message: " + m.getStartLine()); 358 return; 359 } 360 int statusCode = 0; 361 try { 362 statusCode = Integer.parseInt(startLineSegments[1]); 363 } catch (NumberFormatException e) { 364 logw("Could not parse status code for SIP message: " + m.getStartLine()); 365 return; 366 } 367 String toTag = SipMessageParsingUtils.getToTag(m.getHeaderSection()); 368 logi("updateSipDialogState: message has statusCode: " + statusCode + ", and to tag: " 369 + toTag); 370 // If specifically 100 Trying, then do not do anything. 371 if (statusCode <= 100) return; 372 // If 300+, then this dialog has received an error response and should move to closed state. 373 if (statusCode >= 300) { 374 mRcsStats.onSipTransportSessionClosed(mSubId, m.getCallIdParameter(), statusCode, true); 375 d.close(); 376 notifySipDialogState(); 377 return; 378 } 379 if (toTag == null) logw("updateSipDialogState: No to tag for message: " + m); 380 if (statusCode >= 200) { 381 mRcsStats.confirmedSipTransportSession(m.getCallIdParameter(), statusCode); 382 d.confirm(toTag); 383 notifySipDialogState(); 384 return; 385 } 386 // 1XX responses still require updates to dialogs. 387 d.earlyResponse(toTag); 388 notifySipDialogState(); 389 } 390 391 /** 392 * This is a listener to handle SipDialog state of delegate 393 * @param listener {@link SipDialogsStateListener} 394 * @param isNeedNotify It indicates whether the current dialogs state should be notified. 395 */ 396 public void setSipDialogsListener(SipDialogsStateListener listener, 397 boolean isNeedNotify) { 398 mSipDialogsListener = listener; 399 if (listener == null) { 400 return; 401 } 402 if (isNeedNotify) { 403 notifySipDialogState(); 404 } 405 } 406 407 private void notifySipDialogState() { 408 if (mSipDialogsListener == null) { 409 return; 410 } 411 List<SipDialogState> dialogStates = new ArrayList<>(); 412 for (SipDialog d : mTrackedDialogs) { 413 SipDialogState dialog = new SipDialogState.Builder(d.getState()).build(); 414 dialogStates.add(dialog); 415 } 416 mSipDialogsListener.reMappingSipDelegateState(mDelegateKey, dialogStates); 417 } 418 419 private void logi(String log) { 420 Log.i(SipTransportController.LOG_TAG, TAG + ": " + log); 421 mLocalLog.log("[I] " + log); 422 } 423 424 private void logw(String log) { 425 Log.w(SipTransportController.LOG_TAG, TAG + ": " + log); 426 mLocalLog.log("[W] " + log); 427 } 428 } 429