1 /* 2 * Copyright (C) 2010 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 android.net.sip; 18 19 import java.util.ArrayList; 20 import java.util.Arrays; 21 22 /** 23 * An object used to manipulate messages of Session Description Protocol (SDP). 24 * It is mainly designed for the uses of Session Initiation Protocol (SIP). 25 * Therefore, it only handles connection addresses ("c="), bandwidth limits, 26 * ("b="), encryption keys ("k="), and attribute fields ("a="). Currently this 27 * implementation does not support multicast sessions. 28 * 29 * <p>Here is an example code to create a session description.</p> 30 * <pre> 31 * SimpleSessionDescription description = new SimpleSessionDescription( 32 * System.currentTimeMillis(), "1.2.3.4"); 33 * Media media = description.newMedia("audio", 56789, 1, "RTP/AVP"); 34 * media.setRtpPayload(0, "PCMU/8000", null); 35 * media.setRtpPayload(8, "PCMA/8000", null); 36 * media.setRtpPayload(127, "telephone-event/8000", "0-15"); 37 * media.setAttribute("sendrecv", ""); 38 * </pre> 39 * <p>Invoking <code>description.encode()</code> will produce a result like the 40 * one below.</p> 41 * <pre> 42 * v=0 43 * o=- 1284970442706 1284970442709 IN IP4 1.2.3.4 44 * s=- 45 * c=IN IP4 1.2.3.4 46 * t=0 0 47 * m=audio 56789 RTP/AVP 0 8 127 48 * a=rtpmap:0 PCMU/8000 49 * a=rtpmap:8 PCMA/8000 50 * a=rtpmap:127 telephone-event/8000 51 * a=fmtp:127 0-15 52 * a=sendrecv 53 * </pre> 54 * @hide 55 */ 56 public class SimpleSessionDescription { 57 private final Fields mFields = new Fields("voscbtka"); 58 private final ArrayList<Media> mMedia = new ArrayList<Media>(); 59 60 /** 61 * Creates a minimal session description from the given session ID and 62 * unicast address. The address is used in the origin field ("o=") and the 63 * connection field ("c="). See {@link SimpleSessionDescription} for an 64 * example of its usage. 65 */ 66 public SimpleSessionDescription(long sessionId, String address) { 67 address = (address.indexOf(':') < 0 ? "IN IP4 " : "IN IP6 ") + address; 68 mFields.parse("v=0"); 69 mFields.parse(String.format("o=- %d %d %s", sessionId, 70 System.currentTimeMillis(), address)); 71 mFields.parse("s=-"); 72 mFields.parse("t=0 0"); 73 mFields.parse("c=" + address); 74 } 75 76 /** 77 * Creates a session description from the given message. 78 * 79 * @throws IllegalArgumentException if message is invalid. 80 */ 81 public SimpleSessionDescription(String message) { 82 String[] lines = message.trim().replaceAll(" +", " ").split("[\r\n]+"); 83 Fields fields = mFields; 84 85 for (String line : lines) { 86 try { 87 if (line.charAt(1) != '=') { 88 throw new IllegalArgumentException(); 89 } 90 if (line.charAt(0) == 'm') { 91 String[] parts = line.substring(2).split(" ", 4); 92 String[] ports = parts[1].split("/", 2); 93 Media media = newMedia(parts[0], Integer.parseInt(ports[0]), 94 (ports.length < 2) ? 1 : Integer.parseInt(ports[1]), 95 parts[2]); 96 for (String format : parts[3].split(" ")) { 97 media.setFormat(format, null); 98 } 99 fields = media; 100 } else { 101 fields.parse(line); 102 } 103 } catch (Exception e) { 104 throw new IllegalArgumentException("Invalid SDP: " + line); 105 } 106 } 107 } 108 109 /** 110 * Creates a new media description in this session description. 111 * 112 * @param type The media type, e.g. {@code "audio"}. 113 * @param port The first transport port used by this media. 114 * @param portCount The number of contiguous ports used by this media. 115 * @param protocol The transport protocol, e.g. {@code "RTP/AVP"}. 116 */ 117 public Media newMedia(String type, int port, int portCount, 118 String protocol) { 119 Media media = new Media(type, port, portCount, protocol); 120 mMedia.add(media); 121 return media; 122 } 123 124 /** 125 * Returns all the media descriptions in this session description. 126 */ 127 public Media[] getMedia() { 128 return mMedia.toArray(new Media[mMedia.size()]); 129 } 130 131 /** 132 * Encodes the session description and all its media descriptions in a 133 * string. Note that the result might be incomplete if a required field 134 * has never been added before. 135 */ 136 public String encode() { 137 StringBuilder buffer = new StringBuilder(); 138 mFields.write(buffer); 139 for (Media media : mMedia) { 140 media.write(buffer); 141 } 142 return buffer.toString(); 143 } 144 145 /** 146 * Returns the connection address or {@code null} if it is not present. 147 */ 148 public String getAddress() { 149 return mFields.getAddress(); 150 } 151 152 /** 153 * Sets the connection address. The field will be removed if the address 154 * is {@code null}. 155 */ 156 public void setAddress(String address) { 157 mFields.setAddress(address); 158 } 159 160 /** 161 * Returns the encryption method or {@code null} if it is not present. 162 */ 163 public String getEncryptionMethod() { 164 return mFields.getEncryptionMethod(); 165 } 166 167 /** 168 * Returns the encryption key or {@code null} if it is not present. 169 */ 170 public String getEncryptionKey() { 171 return mFields.getEncryptionKey(); 172 } 173 174 /** 175 * Sets the encryption method and the encryption key. The field will be 176 * removed if the method is {@code null}. 177 */ 178 public void setEncryption(String method, String key) { 179 mFields.setEncryption(method, key); 180 } 181 182 /** 183 * Returns the types of the bandwidth limits. 184 */ 185 public String[] getBandwidthTypes() { 186 return mFields.getBandwidthTypes(); 187 } 188 189 /** 190 * Returns the bandwidth limit of the given type or {@code -1} if it is not 191 * present. 192 */ 193 public int getBandwidth(String type) { 194 return mFields.getBandwidth(type); 195 } 196 197 /** 198 * Sets the bandwith limit for the given type. The field will be removed if 199 * the value is negative. 200 */ 201 public void setBandwidth(String type, int value) { 202 mFields.setBandwidth(type, value); 203 } 204 205 /** 206 * Returns the names of all the attributes. 207 */ 208 public String[] getAttributeNames() { 209 return mFields.getAttributeNames(); 210 } 211 212 /** 213 * Returns the attribute of the given name or {@code null} if it is not 214 * present. 215 */ 216 public String getAttribute(String name) { 217 return mFields.getAttribute(name); 218 } 219 220 /** 221 * Sets the attribute for the given name. The field will be removed if 222 * the value is {@code null}. To set a binary attribute, use an empty 223 * string as the value. 224 */ 225 public void setAttribute(String name, String value) { 226 mFields.setAttribute(name, value); 227 } 228 229 /** 230 * This class represents a media description of a session description. It 231 * can only be created by {@link SimpleSessionDescription#newMedia}. Since 232 * the syntax is more restricted for RTP based protocols, two sets of access 233 * methods are implemented. See {@link SimpleSessionDescription} for an 234 * example of its usage. 235 */ 236 public static class Media extends Fields { 237 private final String mType; 238 private final int mPort; 239 private final int mPortCount; 240 private final String mProtocol; 241 private ArrayList<String> mFormats = new ArrayList<String>(); 242 243 private Media(String type, int port, int portCount, String protocol) { 244 super("icbka"); 245 mType = type; 246 mPort = port; 247 mPortCount = portCount; 248 mProtocol = protocol; 249 } 250 251 /** 252 * Returns the media type. 253 */ 254 public String getType() { 255 return mType; 256 } 257 258 /** 259 * Returns the first transport port used by this media. 260 */ 261 public int getPort() { 262 return mPort; 263 } 264 265 /** 266 * Returns the number of contiguous ports used by this media. 267 */ 268 public int getPortCount() { 269 return mPortCount; 270 } 271 272 /** 273 * Returns the transport protocol. 274 */ 275 public String getProtocol() { 276 return mProtocol; 277 } 278 279 /** 280 * Returns the media formats. 281 */ 282 public String[] getFormats() { 283 return mFormats.toArray(new String[mFormats.size()]); 284 } 285 286 /** 287 * Returns the {@code fmtp} attribute of the given format or 288 * {@code null} if it is not present. 289 */ 290 public String getFmtp(String format) { 291 return super.get("a=fmtp:" + format, ' '); 292 } 293 294 /** 295 * Sets a format and its {@code fmtp} attribute. If the attribute is 296 * {@code null}, the corresponding field will be removed. 297 */ 298 public void setFormat(String format, String fmtp) { 299 mFormats.remove(format); 300 mFormats.add(format); 301 super.set("a=rtpmap:" + format, ' ', null); 302 super.set("a=fmtp:" + format, ' ', fmtp); 303 } 304 305 /** 306 * Removes a format and its {@code fmtp} attribute. 307 */ 308 public void removeFormat(String format) { 309 mFormats.remove(format); 310 super.set("a=rtpmap:" + format, ' ', null); 311 super.set("a=fmtp:" + format, ' ', null); 312 } 313 314 /** 315 * Returns the RTP payload types. 316 */ 317 public int[] getRtpPayloadTypes() { 318 int[] types = new int[mFormats.size()]; 319 int length = 0; 320 for (String format : mFormats) { 321 try { 322 types[length] = Integer.parseInt(format); 323 ++length; 324 } catch (NumberFormatException e) { } 325 } 326 return Arrays.copyOf(types, length); 327 } 328 329 /** 330 * Returns the {@code rtpmap} attribute of the given RTP payload type 331 * or {@code null} if it is not present. 332 */ 333 public String getRtpmap(int type) { 334 return super.get("a=rtpmap:" + type, ' '); 335 } 336 337 /** 338 * Returns the {@code fmtp} attribute of the given RTP payload type or 339 * {@code null} if it is not present. 340 */ 341 public String getFmtp(int type) { 342 return super.get("a=fmtp:" + type, ' '); 343 } 344 345 /** 346 * Sets a RTP payload type and its {@code rtpmap} and {@code fmtp} 347 * attributes. If any of the attributes is {@code null}, the 348 * corresponding field will be removed. See 349 * {@link SimpleSessionDescription} for an example of its usage. 350 */ 351 public void setRtpPayload(int type, String rtpmap, String fmtp) { 352 String format = String.valueOf(type); 353 mFormats.remove(format); 354 mFormats.add(format); 355 super.set("a=rtpmap:" + format, ' ', rtpmap); 356 super.set("a=fmtp:" + format, ' ', fmtp); 357 } 358 359 /** 360 * Removes a RTP payload and its {@code rtpmap} and {@code fmtp} 361 * attributes. 362 */ 363 public void removeRtpPayload(int type) { 364 removeFormat(String.valueOf(type)); 365 } 366 367 private void write(StringBuilder buffer) { 368 buffer.append("m=").append(mType).append(' ').append(mPort); 369 if (mPortCount != 1) { 370 buffer.append('/').append(mPortCount); 371 } 372 buffer.append(' ').append(mProtocol); 373 for (String format : mFormats) { 374 buffer.append(' ').append(format); 375 } 376 buffer.append("\r\n"); 377 super.write(buffer); 378 } 379 } 380 381 /** 382 * This class acts as a set of fields, and the size of the set is expected 383 * to be small. Therefore, it uses a simple list instead of maps. Each field 384 * has three parts: a key, a delimiter, and a value. Delimiters are special 385 * because they are not included in binary attributes. As a result, the 386 * private methods, which are the building blocks of this class, all take 387 * the delimiter as an argument. 388 */ 389 private static class Fields { 390 private final String mOrder; 391 private final ArrayList<String> mLines = new ArrayList<String>(); 392 393 Fields(String order) { 394 mOrder = order; 395 } 396 397 /** 398 * Returns the connection address or {@code null} if it is not present. 399 */ 400 public String getAddress() { 401 String address = get("c", '='); 402 if (address == null) { 403 return null; 404 } 405 String[] parts = address.split(" "); 406 if (parts.length != 3) { 407 return null; 408 } 409 int slash = parts[2].indexOf('/'); 410 return (slash < 0) ? parts[2] : parts[2].substring(0, slash); 411 } 412 413 /** 414 * Sets the connection address. The field will be removed if the address 415 * is {@code null}. 416 */ 417 public void setAddress(String address) { 418 if (address != null) { 419 address = (address.indexOf(':') < 0 ? "IN IP4 " : "IN IP6 ") + 420 address; 421 } 422 set("c", '=', address); 423 } 424 425 /** 426 * Returns the encryption method or {@code null} if it is not present. 427 */ 428 public String getEncryptionMethod() { 429 String encryption = get("k", '='); 430 if (encryption == null) { 431 return null; 432 } 433 int colon = encryption.indexOf(':'); 434 return (colon == -1) ? encryption : encryption.substring(0, colon); 435 } 436 437 /** 438 * Returns the encryption key or {@code null} if it is not present. 439 */ 440 public String getEncryptionKey() { 441 String encryption = get("k", '='); 442 if (encryption == null) { 443 return null; 444 } 445 int colon = encryption.indexOf(':'); 446 return (colon == -1) ? null : encryption.substring(0, colon + 1); 447 } 448 449 /** 450 * Sets the encryption method and the encryption key. The field will be 451 * removed if the method is {@code null}. 452 */ 453 public void setEncryption(String method, String key) { 454 set("k", '=', (method == null || key == null) ? 455 method : method + ':' + key); 456 } 457 458 /** 459 * Returns the types of the bandwidth limits. 460 */ 461 public String[] getBandwidthTypes() { 462 return cut("b=", ':'); 463 } 464 465 /** 466 * Returns the bandwidth limit of the given type or {@code -1} if it is 467 * not present. 468 */ 469 public int getBandwidth(String type) { 470 String value = get("b=" + type, ':'); 471 if (value != null) { 472 try { 473 return Integer.parseInt(value); 474 } catch (NumberFormatException e) { } 475 setBandwidth(type, -1); 476 } 477 return -1; 478 } 479 480 /** 481 * Sets the bandwith limit for the given type. The field will be removed 482 * if the value is negative. 483 */ 484 public void setBandwidth(String type, int value) { 485 set("b=" + type, ':', (value < 0) ? null : String.valueOf(value)); 486 } 487 488 /** 489 * Returns the names of all the attributes. 490 */ 491 public String[] getAttributeNames() { 492 return cut("a=", ':'); 493 } 494 495 /** 496 * Returns the attribute of the given name or {@code null} if it is not 497 * present. 498 */ 499 public String getAttribute(String name) { 500 return get("a=" + name, ':'); 501 } 502 503 /** 504 * Sets the attribute for the given name. The field will be removed if 505 * the value is {@code null}. To set a binary attribute, use an empty 506 * string as the value. 507 */ 508 public void setAttribute(String name, String value) { 509 set("a=" + name, ':', value); 510 } 511 512 private void write(StringBuilder buffer) { 513 for (int i = 0; i < mOrder.length(); ++i) { 514 char type = mOrder.charAt(i); 515 for (String line : mLines) { 516 if (line.charAt(0) == type) { 517 buffer.append(line).append("\r\n"); 518 } 519 } 520 } 521 } 522 523 /** 524 * Invokes {@link #set} after splitting the line into three parts. 525 */ 526 private void parse(String line) { 527 char type = line.charAt(0); 528 if (mOrder.indexOf(type) == -1) { 529 return; 530 } 531 char delimiter = '='; 532 if (line.startsWith("a=rtpmap:") || line.startsWith("a=fmtp:")) { 533 delimiter = ' '; 534 } else if (type == 'b' || type == 'a') { 535 delimiter = ':'; 536 } 537 int i = line.indexOf(delimiter); 538 if (i == -1) { 539 set(line, delimiter, ""); 540 } else { 541 set(line.substring(0, i), delimiter, line.substring(i + 1)); 542 } 543 } 544 545 /** 546 * Finds the key with the given prefix and returns its suffix. 547 */ 548 private String[] cut(String prefix, char delimiter) { 549 String[] names = new String[mLines.size()]; 550 int length = 0; 551 for (String line : mLines) { 552 if (line.startsWith(prefix)) { 553 int i = line.indexOf(delimiter); 554 if (i == -1) { 555 i = line.length(); 556 } 557 names[length] = line.substring(prefix.length(), i); 558 ++length; 559 } 560 } 561 return Arrays.copyOf(names, length); 562 } 563 564 /** 565 * Returns the index of the key. 566 */ 567 private int find(String key, char delimiter) { 568 int length = key.length(); 569 for (int i = mLines.size() - 1; i >= 0; --i) { 570 String line = mLines.get(i); 571 if (line.startsWith(key) && (line.length() == length || 572 line.charAt(length) == delimiter)) { 573 return i; 574 } 575 } 576 return -1; 577 } 578 579 /** 580 * Sets the key with the value or removes the key if the value is 581 * {@code null}. 582 */ 583 private void set(String key, char delimiter, String value) { 584 int index = find(key, delimiter); 585 if (value != null) { 586 if (value.length() != 0) { 587 key = key + delimiter + value; 588 } 589 if (index == -1) { 590 mLines.add(key); 591 } else { 592 mLines.set(index, key); 593 } 594 } else if (index != -1) { 595 mLines.remove(index); 596 } 597 } 598 599 /** 600 * Returns the value of the key. 601 */ 602 private String get(String key, char delimiter) { 603 int index = find(key, delimiter); 604 if (index == -1) { 605 return null; 606 } 607 String line = mLines.get(index); 608 int length = key.length(); 609 return (line.length() == length) ? "" : line.substring(length + 1); 610 } 611 } 612 } 613