Home | History | Annotate | Download | only in sip
      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