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