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