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