diff options
author | Chia-chi Yeh <chiachi@android.com> | 2010-09-20 16:34:18 +0800 |
---|---|---|
committer | repo sync <chiachi@android.com> | 2010-09-23 13:31:01 +0800 |
commit | e6c0c109588771a97aba51d06fdf73557b06dfd3 (patch) | |
tree | 7c2853211b7ad3383780c852935d2410613a5161 /voip/java/android/net/sip | |
parent | 7a69aeffda29bd1a7ebc5993eeb4e9ee224f096a (diff) | |
download | frameworks_base-e6c0c109588771a97aba51d06fdf73557b06dfd3.zip frameworks_base-e6c0c109588771a97aba51d06fdf73557b06dfd3.tar.gz frameworks_base-e6c0c109588771a97aba51d06fdf73557b06dfd3.tar.bz2 |
SDP: Add a simple class to help manipulate session descriptions.
Change-Id: I1631ee20e8b4a9ad8e2184356b5d13de66e03db1
Diffstat (limited to 'voip/java/android/net/sip')
-rw-r--r-- | voip/java/android/net/sip/SimpleSessionDescription.java | 612 |
1 files changed, 612 insertions, 0 deletions
diff --git a/voip/java/android/net/sip/SimpleSessionDescription.java b/voip/java/android/net/sip/SimpleSessionDescription.java new file mode 100644 index 0000000..733a5f6 --- /dev/null +++ b/voip/java/android/net/sip/SimpleSessionDescription.java @@ -0,0 +1,612 @@ +/* + * 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; + +/** + * 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. + * + * <p>Here is an example code to create a session description.</p> + * <pre> + * 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", ""); + * </pre> + * <p>Invoking <code>description.encode()</code> will produce a result like the + * one below.</p> + * <pre> + * 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 + * </pre> + * @hide + */ +public class SimpleSessionDescription { + private final Fields mFields = new Fields("voscbtka"); + private final ArrayList<Media> mMedia = new ArrayList<Media>(); + + /** + * 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("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<String> mFormats = new ArrayList<String>(); + + 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 {@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<String> mLines = new ArrayList<String>(); + + 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); + } + } +} |