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.text.TextUtils;
21 
22 import com.android.internal.telephony.SipMessageParsingUtils;
23 
24 import java.time.Instant;
25 import java.util.Objects;
26 import java.util.Set;
27 
28 /**
29  * Track the state of a SIP Dialog.
30  * <p>
31  * SIP Dialogs follow the following initialization flow:
32  * <pre>
33  * (INVITE) ---> EARLY -(2XX)-> CONFIRMED -(BYE)-----> CLOSED
34  *          ^     |   \                           ^
35  *          |--(1XX)   -(3XX-7XX) ----------------|
36  * </pre>
37  * <p> A special note on forking INVITE requests:
38  * During the EARLY phase, a 1XX or 2XX response can carry a To header tag param. This To header
39  * tag param will be set once a INVITE reaches the remote and responds. If the proxy has multiple
40  * endpoints available for the same contact, the INVITE may fork and multiple responses may be
41  * received for the same INVITE request, each with a different To header tag parameter (but the
42  * same call-id). This will generate another SIP dialog within the same SIP session.
43  */
44 public class SipDialog {
45 
46     /**
47      * The device has sent out a dialog starting event and is awaiting a confirmation.
48      */
49     public static final int STATE_EARLY = 0;
50 
51     /**
52      * The device has received a 2XX response to the early dialog.
53      */
54     public static final int STATE_CONFIRMED = 1;
55 
56     /**
57      * The device has received either a 3XX+ response to a pending dialog request or a BYE
58      * request has been sent on this dialog.
59      */
60     public static final int STATE_CLOSED = 2;
61 
62     private final String mBranchId;
63     private final String mCallId;
64     private final String mFromTag;
65     private final Set<String> mAcceptContactFeatureTags;
66     private String mToTag;
67     private int mState = STATE_EARLY;
68     private Instant mLastInteraction;
69 
70     /**
71      * @return A SipDialog instance representing the SIP request.
72      */
fromSipMessage(SipMessage m)73     public static SipDialog fromSipMessage(SipMessage m) {
74         if (!SipMessageParsingUtils.isSipRequest(m.getStartLine())) return null;
75         String fromTag = SipMessageParsingUtils.getFromTag(m.getHeaderSection());
76         Set<String> acceptContactTags = SipMessageParsingUtils.getAcceptContactFeatureTags(
77                 m.getHeaderSection());
78         return new SipDialog(m.getViaBranchParameter(), m.getCallIdParameter(), fromTag,
79                 acceptContactTags);
80     }
81 
82     /**
83      * Track a new SIP dialog, which will be starting in {@link #STATE_EARLY}.
84      *
85      * @param branchId The via branch param of the INVITE request, which is used to match
86      *                 responses.
87      * @param callId   The callId of the SIP dialog.
88      * @param fromTag  The from header's tag parameter.
89      */
SipDialog(String branchId, String callId, String fromTag, Set<String> featureTags)90     private SipDialog(String branchId, String callId, String fromTag, Set<String> featureTags) {
91         mBranchId = branchId;
92         mCallId = callId;
93         mFromTag = fromTag;
94         mAcceptContactFeatureTags = featureTags;
95         mLastInteraction = Instant.now();
96     }
97 
98     /**
99      * @return The call id associated with the SIP dialog.
100      */
getCallId()101     public String getCallId() {
102         return mCallId;
103     }
104 
105     /**
106      * @return the state of the SIP dialog, either {@link #STATE_EARLY},
107      * {@link #STATE_CONFIRMED}, {@link #STATE_CLOSED}.
108      */
getState()109     public int getState() {
110         return mState;
111     }
112 
113     /**
114      * @return The to header's tag parameter if this dialog has gotten a response from the remote
115      * party or {@code null} if it has not.
116      */
getToTag()117     public String getToTag() {
118         return mToTag;
119     }
120 
121     /**
122      * @return The feature tags contained in the "Accept-Contact" header.
123      */
getAcceptContactFeatureTags()124     public Set<String> getAcceptContactFeatureTags() {
125         return mAcceptContactFeatureTags;
126     }
127 
128     /**
129      * @return A new instance with branch param, call-id value, and from tag param populated.
130      */
forkDialog()131     public SipDialog forkDialog() {
132         return new SipDialog(mBranchId, mCallId, mFromTag, mAcceptContactFeatureTags);
133     }
134 
135     /**
136      * A early response has been received (101-199) and contains a to tag, which will create a
137      * dialog.
138      * @param toTag The to tag of the SIP response.
139      */
earlyResponse(String toTag)140     public void earlyResponse(String toTag) {
141         if (TextUtils.isEmpty(toTag) || mState != STATE_EARLY) {
142             // invalid state
143             return;
144         }
145         mLastInteraction = Instant.now();
146         // Only accept To tag if one has not been assigned yet.
147         if (mToTag == null) {
148             mToTag = toTag;
149         }
150     }
151 
152     /**
153      * The early dialog has been confirmed and
154      * @param toTag The To header's tag parameter.
155      */
confirm(String toTag)156     public void confirm(String toTag) {
157         if (mState != STATE_EARLY) {
158             // Invalid state
159             return;
160         }
161         mLastInteraction = Instant.now();
162         mState = STATE_CONFIRMED;
163         // Only accept a To tag if one has not been assigned yet.
164         if (mToTag == null) {
165             mToTag = toTag;
166         }
167     }
168 
169     /**
170      * Close the SIP dialog
171      */
close()172     public void close() {
173         mLastInteraction = Instant.now();
174         mState = STATE_CLOSED;
175     }
176 
177     /**
178      * @return {@code true} if a SIP response's branch, call-id, and from tags match this dialog,
179      * {@code false} if it does not. This may match multiple Dialogs in the case of SIP INVITE
180      * forking.
181      */
isResponseAssociatedWithDialog(SipMessage m)182     public boolean isResponseAssociatedWithDialog(SipMessage m) {
183         if (!mBranchId.equals(m.getViaBranchParameter())) return false;
184         if (!mCallId.equals(m.getCallIdParameter())) return false;
185         String fromTag = SipMessageParsingUtils.getFromTag(m.getHeaderSection());
186         return mFromTag.equals(fromTag);
187     }
188 
189     /**
190      * @return {@code true} if the SIP request is part of the SIP Dialog, {@code false} if it is
191      * not.
192      */
isRequestAssociatedWithDialog(SipMessage m)193     public boolean isRequestAssociatedWithDialog(SipMessage m) {
194         if (!mCallId.equals(m.getCallIdParameter())) return false;
195         String fromTag = SipMessageParsingUtils.getFromTag(m.getHeaderSection());
196         String toTag = SipMessageParsingUtils.getToTag(m.getHeaderSection());
197         // Requests can only be associated if both to and from tag of message are populated. The
198         // dialog's to tag must also be non-null meaning we got a response from the remote.
199         if (fromTag == null || toTag == null || mToTag == null) return false;
200         // For outgoing requests, recorded from tag will match their from tag and for incoming
201         // requests recorded from tag will match their to tag. Same with our recorded to tag.
202         return (mFromTag.equals(fromTag) || mFromTag.equals(toTag))
203                 && (mToTag.equals(toTag) || mToTag.equals(fromTag));
204     }
205 
206     @Override
toString()207     public String toString() {
208         StringBuilder b = new StringBuilder("SipDialog[");
209         switch (mState) {
210             case STATE_EARLY:
211                 b.append("early");
212                 break;
213             case STATE_CONFIRMED:
214                 b.append("confirmed");
215                 break;
216             case STATE_CLOSED:
217                 b.append("closed");
218                 break;
219             default:
220                 b.append(mState);
221         }
222         b.append("] bId=");
223         b.append(mBranchId);
224         b.append(", cId=");
225         b.append(mCallId);
226         b.append(", f=");
227         b.append(mFromTag);
228         b.append(", t=");
229         b.append(mToTag);
230         b.append(", Last Interaction: ");
231         b.append(mLastInteraction);
232         return b.toString();
233     }
234 
235     @Override
equals(Object o)236     public boolean equals(Object o) {
237         if (this == o) return true;
238         if (o == null || getClass() != o.getClass()) return false;
239         SipDialog sipDialog = (SipDialog) o;
240         // Does not include mState and last interaction time, as a dialog is only the same if
241         // its branch, callId, and to/from tags are equal.
242         return mBranchId.equals(sipDialog.mBranchId)
243                 && Objects.equals(mCallId, sipDialog.mCallId)
244                 && Objects.equals(mFromTag, sipDialog.mFromTag)
245                 && Objects.equals(mToTag, sipDialog.mToTag);
246     }
247 
248     @Override
hashCode()249     public int hashCode() {
250         // Does not include mState and last interaction time, as a dialog is only the same if
251         // its branch, callId, and to/from tags are equal.
252         return Objects.hash(mBranchId, mCallId, mFromTag, mToTag);
253     }
254 }
255