/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.net.sip; import java.util.ArrayList; import java.util.Arrays; import java.util.Locale; /** * An object used to manipulate messages of Session Description Protocol (SDP). * It is mainly designed for the uses of Session Initiation Protocol (SIP). * Therefore, it only handles connection addresses ("c="), bandwidth limits, * ("b="), encryption keys ("k="), and attribute fields ("a="). Currently this * implementation does not support multicast sessions. * *

Here is an example code to create a session description.

*
 * SimpleSessionDescription description = new SimpleSessionDescription(
 *     System.currentTimeMillis(), "1.2.3.4");
 * Media media = description.newMedia("audio", 56789, 1, "RTP/AVP");
 * media.setRtpPayload(0, "PCMU/8000", null);
 * media.setRtpPayload(8, "PCMA/8000", null);
 * media.setRtpPayload(127, "telephone-event/8000", "0-15");
 * media.setAttribute("sendrecv", "");
 * 
*

Invoking description.encode() will produce a result like the * one below.

*
 * v=0
 * o=- 1284970442706 1284970442709 IN IP4 1.2.3.4
 * s=-
 * c=IN IP4 1.2.3.4
 * t=0 0
 * m=audio 56789 RTP/AVP 0 8 127
 * a=rtpmap:0 PCMU/8000
 * a=rtpmap:8 PCMA/8000
 * a=rtpmap:127 telephone-event/8000
 * a=fmtp:127 0-15
 * a=sendrecv
 * 
* @hide */ public class SimpleSessionDescription { private final Fields mFields = new Fields("voscbtka"); private final ArrayList mMedia = new ArrayList(); /** * Creates a minimal session description from the given session ID and * unicast address. The address is used in the origin field ("o=") and the * connection field ("c="). See {@link SimpleSessionDescription} for an * example of its usage. */ public SimpleSessionDescription(long sessionId, String address) { address = (address.indexOf(':') < 0 ? "IN IP4 " : "IN IP6 ") + address; mFields.parse("v=0"); mFields.parse(String.format(Locale.US, "o=- %d %d %s", sessionId, System.currentTimeMillis(), address)); mFields.parse("s=-"); mFields.parse("t=0 0"); mFields.parse("c=" + address); } /** * Creates a session description from the given message. * * @throws IllegalArgumentException if message is invalid. */ public SimpleSessionDescription(String message) { String[] lines = message.trim().replaceAll(" +", " ").split("[\r\n]+"); Fields fields = mFields; for (String line : lines) { try { if (line.charAt(1) != '=') { throw new IllegalArgumentException(); } if (line.charAt(0) == 'm') { String[] parts = line.substring(2).split(" ", 4); String[] ports = parts[1].split("/", 2); Media media = newMedia(parts[0], Integer.parseInt(ports[0]), (ports.length < 2) ? 1 : Integer.parseInt(ports[1]), parts[2]); for (String format : parts[3].split(" ")) { media.setFormat(format, null); } fields = media; } else { fields.parse(line); } } catch (Exception e) { throw new IllegalArgumentException("Invalid SDP: " + line); } } } /** * Creates a new media description in this session description. * * @param type The media type, e.g. {@code "audio"}. * @param port The first transport port used by this media. * @param portCount The number of contiguous ports used by this media. * @param protocol The transport protocol, e.g. {@code "RTP/AVP"}. */ public Media newMedia(String type, int port, int portCount, String protocol) { Media media = new Media(type, port, portCount, protocol); mMedia.add(media); return media; } /** * Returns all the media descriptions in this session description. */ public Media[] getMedia() { return mMedia.toArray(new Media[mMedia.size()]); } /** * Encodes the session description and all its media descriptions in a * string. Note that the result might be incomplete if a required field * has never been added before. */ public String encode() { StringBuilder buffer = new StringBuilder(); mFields.write(buffer); for (Media media : mMedia) { media.write(buffer); } return buffer.toString(); } /** * Returns the connection address or {@code null} if it is not present. */ public String getAddress() { return mFields.getAddress(); } /** * Sets the connection address. The field will be removed if the address * is {@code null}. */ public void setAddress(String address) { mFields.setAddress(address); } /** * Returns the encryption method or {@code null} if it is not present. */ public String getEncryptionMethod() { return mFields.getEncryptionMethod(); } /** * Returns the encryption key or {@code null} if it is not present. */ public String getEncryptionKey() { return mFields.getEncryptionKey(); } /** * Sets the encryption method and the encryption key. The field will be * removed if the method is {@code null}. */ public void setEncryption(String method, String key) { mFields.setEncryption(method, key); } /** * Returns the types of the bandwidth limits. */ public String[] getBandwidthTypes() { return mFields.getBandwidthTypes(); } /** * Returns the bandwidth limit of the given type or {@code -1} if it is not * present. */ public int getBandwidth(String type) { return mFields.getBandwidth(type); } /** * Sets the bandwith limit for the given type. The field will be removed if * the value is negative. */ public void setBandwidth(String type, int value) { mFields.setBandwidth(type, value); } /** * Returns the names of all the attributes. */ public String[] getAttributeNames() { return mFields.getAttributeNames(); } /** * Returns the attribute of the given name or {@code null} if it is not * present. */ public String getAttribute(String name) { return mFields.getAttribute(name); } /** * Sets the attribute for the given name. The field will be removed if * the value is {@code null}. To set a binary attribute, use an empty * string as the value. */ public void setAttribute(String name, String value) { mFields.setAttribute(name, value); } /** * This class represents a media description of a session description. It * can only be created by {@link SimpleSessionDescription#newMedia}. Since * the syntax is more restricted for RTP based protocols, two sets of access * methods are implemented. See {@link SimpleSessionDescription} for an * example of its usage. */ public static class Media extends Fields { private final String mType; private final int mPort; private final int mPortCount; private final String mProtocol; private ArrayList mFormats = new ArrayList(); private Media(String type, int port, int portCount, String protocol) { super("icbka"); mType = type; mPort = port; mPortCount = portCount; mProtocol = protocol; } /** * Returns the media type. */ public String getType() { return mType; } /** * Returns the first transport port used by this media. */ public int getPort() { return mPort; } /** * Returns the number of contiguous ports used by this media. */ public int getPortCount() { return mPortCount; } /** * Returns the transport protocol. */ public String getProtocol() { return mProtocol; } /** * Returns the media formats. */ public String[] getFormats() { return mFormats.toArray(new String[mFormats.size()]); } /** * Returns the {@code fmtp} attribute of the given format or * {@code null} if it is not present. */ public String getFmtp(String format) { return super.get("a=fmtp:" + format, ' '); } /** * Sets a format and its {@code fmtp} attribute. If the attribute is * {@code null}, the corresponding field will be removed. */ public void setFormat(String format, String fmtp) { mFormats.remove(format); mFormats.add(format); super.set("a=rtpmap:" + format, ' ', null); super.set("a=fmtp:" + format, ' ', fmtp); } /** * Removes a format and its {@code fmtp} attribute. */ public void removeFormat(String format) { mFormats.remove(format); super.set("a=rtpmap:" + format, ' ', null); super.set("a=fmtp:" + format, ' ', null); } /** * Returns the RTP payload types. */ public int[] getRtpPayloadTypes() { int[] types = new int[mFormats.size()]; int length = 0; for (String format : mFormats) { try { types[length] = Integer.parseInt(format); ++length; } catch (NumberFormatException e) { } } return Arrays.copyOf(types, length); } /** * Returns the {@code rtpmap} attribute of the given RTP payload type * or {@code null} if it is not present. */ public String getRtpmap(int type) { return super.get("a=rtpmap:" + type, ' '); } /** * Returns the {@code fmtp} attribute of the given RTP payload type or * {@code null} if it is not present. */ public String getFmtp(int type) { return super.get("a=fmtp:" + type, ' '); } /** * Sets a RTP payload type and its {@code rtpmap} and {@code fmtp} * attributes. If any of the attributes is {@code null}, the * corresponding field will be removed. See * {@link SimpleSessionDescription} for an example of its usage. */ public void setRtpPayload(int type, String rtpmap, String fmtp) { String format = String.valueOf(type); mFormats.remove(format); mFormats.add(format); super.set("a=rtpmap:" + format, ' ', rtpmap); super.set("a=fmtp:" + format, ' ', fmtp); } /** * Removes a RTP payload and its {@code rtpmap} and {@code fmtp} * attributes. */ public void removeRtpPayload(int type) { removeFormat(String.valueOf(type)); } private void write(StringBuilder buffer) { buffer.append("m=").append(mType).append(' ').append(mPort); if (mPortCount != 1) { buffer.append('/').append(mPortCount); } buffer.append(' ').append(mProtocol); for (String format : mFormats) { buffer.append(' ').append(format); } buffer.append("\r\n"); super.write(buffer); } } /** * This class acts as a set of fields, and the size of the set is expected * to be small. Therefore, it uses a simple list instead of maps. Each field * has three parts: a key, a delimiter, and a value. Delimiters are special * because they are not included in binary attributes. As a result, the * private methods, which are the building blocks of this class, all take * the delimiter as an argument. */ private static class Fields { private final String mOrder; private final ArrayList mLines = new ArrayList(); Fields(String order) { mOrder = order; } /** * Returns the connection address or {@code null} if it is not present. */ public String getAddress() { String address = get("c", '='); if (address == null) { return null; } String[] parts = address.split(" "); if (parts.length != 3) { return null; } int slash = parts[2].indexOf('/'); return (slash < 0) ? parts[2] : parts[2].substring(0, slash); } /** * Sets the connection address. The field will be removed if the address * is {@code null}. */ public void setAddress(String address) { if (address != null) { address = (address.indexOf(':') < 0 ? "IN IP4 " : "IN IP6 ") + address; } set("c", '=', address); } /** * Returns the encryption method or {@code null} if it is not present. */ public String getEncryptionMethod() { String encryption = get("k", '='); if (encryption == null) { return null; } int colon = encryption.indexOf(':'); return (colon == -1) ? encryption : encryption.substring(0, colon); } /** * Returns the encryption key or {@code null} if it is not present. */ public String getEncryptionKey() { String encryption = get("k", '='); if (encryption == null) { return null; } int colon = encryption.indexOf(':'); return (colon == -1) ? null : encryption.substring(0, colon + 1); } /** * Sets the encryption method and the encryption key. The field will be * removed if the method is {@code null}. */ public void setEncryption(String method, String key) { set("k", '=', (method == null || key == null) ? method : method + ':' + key); } /** * Returns the types of the bandwidth limits. */ public String[] getBandwidthTypes() { return cut("b=", ':'); } /** * Returns the bandwidth limit of the given type or {@code -1} if it is * not present. */ public int getBandwidth(String type) { String value = get("b=" + type, ':'); if (value != null) { try { return Integer.parseInt(value); } catch (NumberFormatException e) { } setBandwidth(type, -1); } return -1; } /** * Sets the bandwith limit for the given type. The field will be removed * if the value is negative. */ public void setBandwidth(String type, int value) { set("b=" + type, ':', (value < 0) ? null : String.valueOf(value)); } /** * Returns the names of all the attributes. */ public String[] getAttributeNames() { return cut("a=", ':'); } /** * Returns the attribute of the given name or {@code null} if it is not * present. */ public String getAttribute(String name) { return get("a=" + name, ':'); } /** * Sets the attribute for the given name. The field will be removed if * the value is {@code null}. To set a binary attribute, use an empty * string as the value. */ public void setAttribute(String name, String value) { set("a=" + name, ':', value); } private void write(StringBuilder buffer) { for (int i = 0; i < mOrder.length(); ++i) { char type = mOrder.charAt(i); for (String line : mLines) { if (line.charAt(0) == type) { buffer.append(line).append("\r\n"); } } } } /** * Invokes {@link #set} after splitting the line into three parts. */ private void parse(String line) { char type = line.charAt(0); if (mOrder.indexOf(type) == -1) { return; } char delimiter = '='; if (line.startsWith("a=rtpmap:") || line.startsWith("a=fmtp:")) { delimiter = ' '; } else if (type == 'b' || type == 'a') { delimiter = ':'; } int i = line.indexOf(delimiter); if (i == -1) { set(line, delimiter, ""); } else { set(line.substring(0, i), delimiter, line.substring(i + 1)); } } /** * Finds the key with the given prefix and returns its suffix. */ private String[] cut(String prefix, char delimiter) { String[] names = new String[mLines.size()]; int length = 0; for (String line : mLines) { if (line.startsWith(prefix)) { int i = line.indexOf(delimiter); if (i == -1) { i = line.length(); } names[length] = line.substring(prefix.length(), i); ++length; } } return Arrays.copyOf(names, length); } /** * Returns the index of the key. */ private int find(String key, char delimiter) { int length = key.length(); for (int i = mLines.size() - 1; i >= 0; --i) { String line = mLines.get(i); if (line.startsWith(key) && (line.length() == length || line.charAt(length) == delimiter)) { return i; } } return -1; } /** * Sets the key with the value or removes the key if the value is * {@code null}. */ private void set(String key, char delimiter, String value) { int index = find(key, delimiter); if (value != null) { if (value.length() != 0) { key = key + delimiter + value; } if (index == -1) { mLines.add(key); } else { mLines.set(index, key); } } else if (index != -1) { mLines.remove(index); } } /** * Returns the value of the key. */ private String get(String key, char delimiter) { int index = find(key, delimiter); if (index == -1) { return null; } String line = mLines.get(index); int length = key.length(); return (line.length() == length) ? "" : line.substring(length + 1); } } }