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